const std = @import("std");
const wayland = @import("zig-wayland/wayland.zig");
const zig_args = @import("zig-args");
const wl = wayland.client.wl;
const xdg = wayland.client.xdg;
const zxdg = wayland.client.zxdg;
const zwlr = wayland.client.zwlr;
const list = wayland.server.wl.list;

const Utils = @import("box.zig");
const Point = Utils.Point;
const Box = Utils.Box;
const Boxes = Utils.Boxes;
const Size = Utils.Size;
const Selection = Utils.Selection;

const xkb = @cImport({
    @cInclude("xkbcommon/xkbcommon.h");
});

const Cairo = @cImport({
    @cInclude("cairo/cairo.h");
});


const Buffer = struct {
    const Self = @This();

    buffer: *wl.Buffer = undefined,
    cairo: struct {
        surface: *Cairo.cairo_surface_t = undefined,
        ctx: *Cairo.cairo_t = undefined,
    } = .{},
    size: Size = .{},
    data: []u8 = undefined,
    in_use: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false),

    fn randomName() [13]u8 {
        var name = [_]u8{'/', 'z', 'l', 'u','r','p','-','0','0','0','0','0','0'};

        var rng = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
        for (name[7..]) |*c| {
            c.* = rng.random().intRangeAtMost(u8, 'A', 'Z');
        }

        return name;
    }

    fn init(self: *Self, shm: *wl.Shm, size: Size) !void {
        const name = randomName();
        const stride = size.width * 4;
        const len = stride * size.height;

        const fd = try std.os.memfd_create(&name, 0);
        defer std.os.close(fd);
        try std.os.ftruncate(fd, len);

        const data = try std.os.mmap(
            null,
            len,
            std.os.PROT.READ | std.os.PROT.WRITE,
            std.os.MAP.SHARED,
            fd,
            0,
        );

        const pool = try shm.createPool(fd, @intCast(i32, len));
        defer pool.destroy();

        const buffer = try pool.createBuffer(
            0,
            @intCast(i32, size.width),
            @intCast(i32, size.height),
            @intCast(i32, stride),
            .argb8888,
        );

        const surface = Cairo.cairo_image_surface_create_for_data(
            @ptrCast([*c]u8, data),
            Cairo.CAIRO_FORMAT_ARGB32,
            @intCast(c_int, size.width),
            @intCast(c_int, size.height),
            @intCast(c_int, stride),
        ) orelse return error.NoCairoSurface;

        const cairo_ctx = Cairo.cairo_create(surface) orelse return error.NoCairoContext;

        self.* = .{
            .buffer = buffer,
            .size = size,
            .data = data,
            .cairo = .{
                .surface = surface,
                .ctx = cairo_ctx,
            },
        };

        self.buffer.setListener(*Self, bufferListener, self);
    }

    fn deinit(self: *Self) void {
        std.os.munmap(@alignCast(0x1000, self.data));
        self.buffer.destroy();

        Cairo.cairo_destroy(self.cairo.ctx);
        Cairo.cairo_surface_destroy(self.cairo.surface);
    }

    fn bufferListener(buffer: *wl.Buffer, event: wl.Buffer.Event, self: *Self) void {
        _ = buffer;

        switch (event) {
            .release => {
                self.in_use.store(false, .Monotonic);
            },
        }
    }
};

const Buffers = struct {
    const Self = @This();
    state: *State.Init,
    buffers: [2]?Buffer = undefined,

    fn init(self: *Self, state: *State.Init) !void {
        self.* = Self{
            .state = state,
        };

        for (self.buffers) |*buffer| {
            buffer.* = null;
        }
    }

    // returns any already initialized and not currently used buffer
    fn getAvailableBuffer(self: *Self, size: Size) ?*Buffer {
        for (self.buffers) |*buffer| {
            if (buffer.*) |*buf| {
                if (!buf.in_use.load(.Monotonic)) {
                    if (buf.size.width != size.width or buf.size.height != size.height) {
                        buf.deinit();
                        buf.* = .{};
                        buf.init(self.state.shm, size) catch {
                            buffer.* = null;
                            continue;
                        };
                    }
                    return buf;
                }
            }
        }

        for (self.buffers) |*buffer| {
            if (buffer.* == null) {
                buffer.* = .{};
                buffer.*.?.init(self.state.shm, size) catch {
                    buffer.* = null;
                    continue;
                };
                return &buffer.*.?;
            }
        }

        return null;
    }
};

test "buffers" {
    std.testing.refAllDecls(Buffers);
}

fn ListItem(comptime T: type) type {
    return struct {
        const Self = @This();

        link: list.Link,
        value: T = undefined,

        fn init(self: *Self, value: T) void {
            self.value = value;
        }

        fn Head() type {
            return list.Head(Self, "link");
        }

        fn fromLink(link: *list.Link) *Self {
            return @fieldParentPtr(Self, "link", link);
        }

        fn fromValue(value: *T) *Self {
            return @fieldParentPtr(Self, "value", value);
        }

        /// automatically calls `deinit()` on `data` if a function with that name exists
        fn remove(self: *Self) void {
            if (@hasDecl(T, "deinit")) {
                self.value.deinit();
            }

            self.link.remove();
        }

        fn next(self: *Self) ?*list.Link {
            self.link.next();
        }

        fn prev(self: *Self) ?*list.Link {
            self.link.prev();
        }

        fn getLink(self: *Self) *list.Link {
            return &self.link;
        }

        fn get(self: *Self) *T {
            return &self.value;
        }

        fn getConst(self: *const Self) *const T {
            return &self.value;
        }
    };
}

const Output = struct {
    const Self = @This();

    state: *State.Init = undefined,
    output: *wl.Output,

    surface: *wl.Surface = undefined,
    layer_surface: *zwlr.LayerSurfaceV1 = undefined,
    xdg_output: *zxdg.OutputV1 = undefined,

    geometry: Box = Box.default(),
    logical_geometry: Box = Box.default(),
    scale: i32 = 1,

    size: Size = Size{},

    configured: bool = false,
    needs_redraw: bool = false,

    buffers: Buffers = undefined,
    frame_callback: ?*wl.Callback = null,

    fn init(self: *Self, state: *State.Init) !void {
        std.debug.print("output.init()\n", .{});

        self.state = state;
        try self.buffers.init(state);

        self.output.setListener(*Self, outputListener, self);

        self.surface = try state.compositor.createSurface();

        self.layer_surface = try state.layer_shell.getLayerSurface(
            self.surface,
            self.output,
            .overlay,
            "selection",
        );
        self.layer_surface.setListener(*Self, layerSurfaceListener, self);

        self.xdg_output = try state.xdg_output_manager.getXdgOutput(self.output);
        self.xdg_output.setListener(*Self, xdgOutputListener, self);

        self.layer_surface.setAnchor(.{.top = true, .left = true, .right = true, .bottom = true,});
        self.layer_surface.setKeyboardInteractivity(1);
        self.layer_surface.setExclusiveZone(-1);
        self.surface.commit();
    }

    fn deinit(self: *Self) void {
        self.output.destroy();
    }


    fn setSourceU32(c: *Cairo.cairo_t, color: u32) void {
        Cairo.cairo_set_source_rgba(
            c,
            @intToFloat(f32, color >> (3 * 8) & 0xff) / 255.0,
            @intToFloat(f32, color >> (2 * 8) & 0xff) / 255.0,
            @intToFloat(f32, color >> (1 * 8) & 0xff) / 255.0,
            @intToFloat(f32, color >> (0 * 8) & 0xff) / 255.0,
        );
    }

    fn drawRect(buffer: *Buffer, box: Box, color: u32) void {
        const cairo = buffer.cairo.ctx;
        setSourceU32(cairo, color);
        Cairo.cairo_rectangle(
            cairo,
            box.getX(f64),
            box.getY(f64),
            box.getWidth(f64),
            box.getHeight(f64),
        );
    }

    fn render(self: *Self, buffer: *Buffer) void {
        const cairo = &buffer.cairo;
        const opts = self.state.getOptions();

        // clear screen
        Cairo.cairo_set_operator(cairo.ctx, Cairo.CAIRO_OPERATOR_SOURCE);
        setSourceU32(cairo.ctx, opts.background);
        Cairo.cairo_paint(cairo.ctx);

        // draw in_boxes
        if (self.state.config.in_boxes) |boxes| {
            std.debug.print("in_boxes: {}\n", .{boxes});
            var iter = boxes.iter();
            while (iter.next()) |box| {
                if (box.intersects(&self.logical_geometry)) {
                    const corrected_box = box.translated(self.logical_geometry.position.inverted());

                    drawRect(buffer, corrected_box, opts.choice);
                    Cairo.cairo_fill(cairo.ctx);
                }
            }
        }


        // draw selection for each seat
        var seat_iter = self.state.seats.iterator(.forward);
        while (seat_iter.next()) |link| {
            const seat = link.get();
            if (seat.pointer == null) {
                continue;
            }

            if (seat.pointer.?.selection.getSelectionBox()) |box| {
                // draw inner box
                const corrected_box = box.translated(self.logical_geometry.position.inverted());
                std.debug.print("selection: {}\n", .{corrected_box});

                drawRect(buffer, corrected_box, opts.selection);
                Cairo.cairo_fill(cairo.ctx);

                Cairo.cairo_set_line_width(cairo.ctx, @intToFloat(f64, opts.@"border-weight"));
                drawRect(buffer, corrected_box, opts.border);
                Cairo.cairo_stroke(cairo.ctx);

                // TODO: draw dimensions
            }
        }
    }

    fn commitFrame(self: *Self) void {
        const size = Size{
            .width = self.size.width * @intCast(u32, self.scale),
            .height = self.size.height * @intCast(u32, self.scale),
        };

        // FIXME: maybe somehow make this retry to get another buffer if the one
        // that was returned has just become in-use?
        if (self.buffers.getAvailableBuffer(size)) |buffer| {
            if (buffer.in_use.compareAndSwap(false, true, .Release, .Monotonic) == null) {
                Cairo.cairo_identity_matrix(buffer.cairo.ctx);
                Cairo.cairo_scale(
                    buffer.cairo.ctx,
                    @intToFloat(f64, self.scale),
                    @intToFloat(f64, self.scale),
                );

                // RENDER
                self.render(buffer);

                if (self.frame_callback == null) {
                    self.frame_callback = self.surface.frame() catch null;
                    if (self.frame_callback) |frame| {
                        frame.setListener(*Self, frameCallbackListener, self);
                    }
                }

                self.surface.attach(buffer.buffer, 0, 0);
                self.surface.damage(0, 0,
                                    @intCast(i32,  self.size.width),
                                    @intCast(i32,  self.size.height),
                                    );
                self.surface.setBufferScale(self.scale);
                self.surface.commit();

                self.needs_redraw = false;
            } else {
                std.debug.print("failed to CAS in_use.\n", .{});
            }
        } else {
            std.debug.print("No buffer available!\n", .{});
        }
    }

    fn requestRedraw(self: *Self) !void {
        self.needs_redraw = true;

        if (self.frame_callback == null) {
            self.frame_callback = try self.surface.frame();
            self.frame_callback.?.setListener(*Self, frameCallbackListener, self);
            self.surface.commit();
        }
    }

    fn frameCallbackListener(frame: *wl.Callback, event: wl.Callback.Event, self: *Self) void {
        switch (event) {
            .done => {
                frame.destroy();
                self.frame_callback = null;

                if (self.needs_redraw) {
                    self.commitFrame();
                }

                std.debug.print("frame_callback done\n", .{});
            },
        }
    }

    fn xdgOutputListener(output: *zxdg.OutputV1, event: zxdg.OutputV1.Event, self: *Self) void {
        _ = output;

        std.debug.print("zxdg_output listener: {}\n", .{event});

        switch (event) {
            .logical_position => |pos| {
                self.logical_geometry.position = .{.x = pos.x, .y = pos.y,};
            },
            .logical_size => |size| {
                self.logical_geometry.extents = .{
                    .width = @intCast(u32, size.width),
                    .height = @intCast(u32, size.height),
                };
            },
            else => {},
        }
    }

    fn layerSurfaceListener(
        layer: *zwlr.LayerSurfaceV1,
        event: zwlr.LayerSurfaceV1.Event,
        self: *Self,
    ) void {
        std.debug.print("zwlr_layer_surface listener: {}\n", .{event});

        switch (event) {
            .configure => |cfg|{
                self.configured = true;
                self.size = .{
                    .width = cfg.width,
                    .height = cfg.height,
                };

                layer.ackConfigure(cfg.serial);
                self.commitFrame();
            },
            .closed => {
                self.state.removeOutput(self);
            },
        }
    }

    fn outputListener(output: *wl.Output, event: wl.Output.Event, self: *Self) void {
        _ = output;

        std.debug.print("wl_output listener: {}\n", .{event});

        switch (event) {
            .geometry => |geom| {
                self.geometry.position = .{.x = geom.x, .y = geom.y};
            },
            .mode => |mode| {
                if (!mode.flags.current) {
                    self.geometry.extents = .{
                        .width = @intCast(u32,  mode.width),
                        .height = @intCast(u32, mode.height),
                    };
                }
            },
            .scale => |scale| {
                self.scale = scale.factor;
            },
            else => {
                std.debug.print("wl_output listener: unhandled\n", .{});
            },
        }
    }
};

const Seat = struct {
    const Self = @This();

    state: *State.Init = undefined,
    seat: *wl.Seat,

    keyboard: ?struct {
        keyboard: *wl.Keyboard,
        xkb: ?struct {
            keymap: *xkb.xkb_keymap,
            state: *xkb.xkb_state,
        } = null,
    } = null,

    pointer: ?struct {
        pointer: *wl.Pointer,
        current_surface_offset: ?Point = null,
        stage: SelectionStage = .Edit,
        selection: SelectionState = SelectionState.None(),
    } = null,


    // awful naming but thats life innit
    // this encodes whether the selection is actively being edited or moved as a whole
    const SelectionStage = enum {
        Edit,
        Move,
    };

    const SelectionTag = enum {
        none,
        pre,
        updating,
        post,
    };

    const SelectionState = union(SelectionTag) {

        none: void,
        pre: Point,
        updating: Selection,
        post: Box,

        fn None() SelectionState {
            return SelectionState.none;
        }

        fn translate(self: *SelectionState, delta: Point) void {
            switch (self.*) {
                .pre => |*point| {
                    point.add(delta);
                },
                .updating => |*selection| {
                    selection.start.add(delta);
                    selection.end.add(delta);
                },
                .post => |*box| {
                    box.translate(delta);
                },
                else => {},
            }
        }

        fn getSelectionBox(self: SelectionState) ?Box {
            switch (self) {
                .updating => |selection| {
                    return selection.asBox();
                },
                .post => |box| {
                    return box;
                },
                else => {return null;},
            }
        }
    };

    fn init(self: *Self, state: *State.Init) void {
        self.state = state;

        std.debug.print("seat.init()\n", .{});
        self.seat.setListener(*Self, seatListener, self);
    }

    fn calculateRedraws(self: *Self) void {
        if (self.pointer) |ptr| {
            const box = ptr.selection.getSelectionBox();

            var output_iter = self.state.outputs.iterator(.forward);
            while (output_iter.next()) |link| {
                const output = link.get();
                if (box == null or output.logical_geometry.intersects(&box.?)) {
                    output.requestRedraw() catch {};
                }
            }
        }
    }

    fn pointerListener(pointer: *wl.Pointer, event: wl.Pointer.Event, self: *Self) void {
        _ = pointer;

        switch (event) {
            .enter => |enter| {
                if (self.state.getOutputForSurface(enter.surface.?)) |output| {
                    self.pointer.?.current_surface_offset = output.logical_geometry.position;
                }

                std.debug.print("surface offset: {?}\n", .{self.pointer.?.current_surface_offset});
            },
            .leave => {
                self.pointer.?.current_surface_offset = null;
            },
            .motion => |motion| {
                const x = motion.surface_x.toInt();
                const y = motion.surface_y.toInt();
                const xy = Point{.x = x, .y = y,};

                if (self.pointer.?.current_surface_offset) |offset| {
                    const point = xy.added(offset);

                    switch (self.pointer.?.stage) {
                        .Edit => {
                            switch (self.pointer.?.selection) {
                                .pre => {
                                    self.pointer.?.selection = .{.updating = .{.start = point, .end = point}};
                                    return self.calculateRedraws();
                                },
                                .updating => |*selection| {
                                    selection.end = point;
                                    return self.calculateRedraws();
                                },
                                else => {},
                            }
                        },
                        .Move => {
                            const delta = blk: {
                                switch (self.pointer.?.selection) {
                                    .pre => |end| {
                                        break :blk point.added(end.inverted());
                                    },
                                    .updating => |selection| {
                                        break :blk point.added(selection.end.inverted());
                                    },
                                    else => {break :blk null;},
                                }
                            };

                            if (delta != null) {
                                self.pointer.?.selection.translate(delta.?);
                                return self.calculateRedraws();
                            }
                        },
                    }

                }
            },
            .button => |button| {
                switch (button.state) {
                    .pressed => {
                        self.pointer.?.selection = SelectionState{.pre = .{}};
                    },
                    .released => {
                        switch (self.pointer.?.selection) {
                            .updating => |selection| {
                                const box = selection.asBox();
                                self.pointer.?.selection = .{.post = box};
                                return self.calculateRedraws();
                            },
                            else => {},
                        }
                    },
                    else => {},
                }
            },
            else => {},
        }

    }

    fn keyboardListener(keyboard: *wl.Keyboard, event: wl.Keyboard.Event, self: *Self) void {
        _ = keyboard;

        switch (event) {
            .keymap => |keymap_event| {
                const keymap = blk: {
                    switch (keymap_event.format) {
                        .no_keymap => {
                            break :blk xkb.xkb_keymap_new_from_names(
                                self.state.xkb_context,
                                null,
                                xkb.XKB_KEYMAP_COMPILE_NO_FLAGS,
                            );
                        },
                        .xkb_v1 => {
                            const buffer = std.os.mmap(
                                null,
                                keymap_event.size,
                                std.os.PROT.READ,
                                std.os.MAP.PRIVATE,
                                keymap_event.fd,
                                0,
                            ) catch break :blk null;
                            defer std.os.munmap(buffer);
                            defer std.os.close(keymap_event.fd);

                            break :blk xkb.xkb_keymap_new_from_buffer(
                                self.state.xkb_context,
                                @ptrCast([*c]const u8, buffer),
                                keymap_event.size - 1,
                                xkb.XKB_KEYMAP_FORMAT_TEXT_V1,
                                xkb.XKB_KEYMAP_COMPILE_NO_FLAGS,
                            );

                        },
                        else => unreachable,
                    }
                };

                if (keymap) |map| {
                    const state = xkb.xkb_state_new(map);
                    if (state) |state_| {
                        // SAFETY: keyboard cant be null because we are in the keyboard listener
                        self.keyboard.?.xkb = .{
                            .keymap = map,
                            .state = state_,
                        };
                    }
                }
            },
            .key => |key| {
                if (self.keyboard.?.xkb) |xkb_| {
                    const keysym = xkb.xkb_state_key_get_one_sym(xkb_.state, key.key + 8);

                    // const buf_len = 32;
                    // const buf = self.state.ally.alloc(u8, buf_len) catch null;
                    // if (buf) |buffer| {
                    //     _ = xkb.xkb_keysym_get_name(keysym, @ptrCast([*c]u8, buffer), buf_len);
                    //     std.debug.print("key: {s} {s}\n", .{buffer, if (key.state == .pressed) "pressed" else "released"});
                    // }

                    switch (key.state) {
                        .pressed => {
                            switch (keysym) {
                                xkb.XKB_KEY_Escape, xkb.XKB_KEY_q => {

                                    if (self.pointer) |*ptr| {
                                        switch (ptr.selection) {
                                            .none => {
                                                self.state.running = false;
                                                return;
                                            },
                                            else => {
                                                ptr.selection = .none;
                                                return self.calculateRedraws();
                                            }
                                        }
                                    }

                                },
                                xkb.XKB_KEY_space => {
                                    if (self.pointer) |*ptr| {
                                        ptr.stage = .Move;
                                    }
                                },
                                else => {},
                            }
                        },
                        .released => {
                            switch (keysym) {
                                xkb.XKB_KEY_space => {
                                    if (self.pointer) |*ptr| {
                                        ptr.stage = .Edit;
                                    }
                                },
                                else => {},
                            }
                        },
                        else => {},
                    }
                }
            },
            else => {},
        }
    }

    fn seatListener(seat: *wl.Seat, event: wl.Seat.Event, self: *Self) void {
        switch (event) {
            .capabilities => |value| {
                std.debug.print("seat capabilities: {}\n", .{value.capabilities});
                const capabilities = value.capabilities;
                if (capabilities.keyboard) {
                    const keyboard = seat.getKeyboard() catch return;
                    self.keyboard = .{
                        .keyboard = keyboard,
                    };

                    keyboard.setListener(*Self, keyboardListener, self);
                }

                if (capabilities.pointer) {
                    const pointer = seat.getPointer() catch return;
                    self.pointer = .{
                        .pointer = pointer,
                    };

                    pointer.setListener(*Self, pointerListener, self);
                }
            },
            else => {},
        }
    }

    fn deinit(self: *Self) void {
        if (self.keyboard) |keyboard| {
            keyboard.keyboard.destroy();

            if (keyboard.xkb) |kb| {
                xkb.xkb_state_unref(kb.state);
                xkb.xkb_keymap_unref(kb.keymap);
            }
        }

        if (self.pointer) |pointer| {
            pointer.pointer.destroy();
        }


        self.seat.destroy();
    }
};

/// helper type to handle dynamic dispatch listeners whatever they're called
fn Listener(comptime T: type, comptime D: type) type {
    return struct {
        const Self = @This();
        const Fn = *const fn (obj: *T, event: T.Event, data: *D) void;

        data: *D,
        callback: Fn,

        fn create(data: *D, callback: Fn) Self {
            return Self{
                .data = data,
                .callback = callback,
            };
        }

        fn listener(obj: *T, event: T.Event, self: *Self) void {
            return self.callback(obj, event, self.data);
        }

        fn set(self: *Self, obj: *T) void {
            obj.setListener(*Self, listener, self);
        }

        fn cast(
            self: *Self,
            comptime D2: type,
            new_data: *D2,
            new_callback: Listener(T, D2).Fn,
        ) *Listener(T, D2) {
            const other = @ptrCast(*Listener(T, D2), self);
            other.data = new_data;
            other.callback = new_callback;
            return other;
        }
    };
}

const State = struct {
    const Uninit = struct {
        const Self = @This();

        ally: std.mem.Allocator = std.heap.c_allocator,

        dpy: *wl.Display = undefined,
        registry: *wl.Registry = undefined,

        registry_listener: *Listener(wl.Registry, Self) = undefined,

        shm: ?*wl.Shm = null,
        compositor: ?*wl.Compositor = null,
        layer_shell: ?*zwlr.LayerShellV1 = null,
        xdg_output_manager: ?*zxdg.OutputManagerV1 = null,

        seats: *ListItem(Seat).Head() = undefined,
        outputs: *ListItem(Output).Head() = undefined,

        fn create(ally: std.mem.Allocator) !*Self {

            const dpy = try wl.Display.connect(null);
            const registry = try dpy.getRegistry();

            const self = try ally.create(Self);

            const listener = try ally.create(Listener(wl.Registry, Self));

            listener.* = .{
                .data = self,
                .callback = registryListener,
            };

            self.* = .{
                .ally = ally,
                .dpy = dpy,
                .registry = registry,
                .registry_listener = listener,
                .seats = try ally.create(ListItem(Seat).Head()),
                .outputs = try ally.create(ListItem(Output).Head()),
            };

            self.seats.init();
            self.outputs.init();

            self.registry_listener.set(self.registry);
            if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed;

            return self;
        }

        fn deinit(self: *Self) void {
            defer self.ally.destroy(self);

            var seats_iter = self.seats.safeIterator(.forward);
            while (seats_iter.next()) |next| {
                next.remove();
                self.ally.destroy(next);
            }
            self.ally.destroy(self.seats);


            var outputs_iter = self.outputs.safeIterator(.forward);
            while (outputs_iter.next()) |next| {
                next.remove();
                self.ally.destroy(next);
            }
            self.ally.destroy(self.outputs);

            if (self.shm) |p| {
                p.destroy();
            }
            if (self.compositor) |p| {
                p.destroy();
            }
            if (self.layer_shell) |p| {
                p.destroy();
            }
            if (self.xdg_output_manager) |p| {
                p.destroy();
            }

            self.registry.destroy();
            self.ally.destroy(
                self.registry_listener
            );
        }

        fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void {
            switch (event) {
                .global => |global| {
                    std.debug.print("global: {s}\n", .{global.interface});

                    if (std.cstr.cmp(global.interface, wl.Compositor.getInterface().name) == 0) {
                        self.compositor = registry.bind(global.name, wl.Compositor, 4) catch return;
                    } else if (std.cstr.cmp(global.interface, wl.Shm.getInterface().name) == 0) {
                        self.shm = registry.bind(global.name, wl.Shm, 1) catch return;
                    } else if (std.cstr.cmp(global.interface, zwlr.LayerShellV1.getInterface().name) == 0) {
                        self.layer_shell = registry.bind(global.name, zwlr.LayerShellV1, 1) catch return;
                    } else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0) {
                        self.xdg_output_manager =
                            registry.bind(global.name, zxdg.OutputManagerV1, 2) catch
                            return;

                    } else if (std.cstr.cmp(global.interface, wl.Seat.getInterface().name) == 0) {
                        const seat = registry.bind(global.name, wl.Seat, 1) catch return;

                        if (self.ally.create(ListItem(Seat)) catch null) |ele| {
                            ele.init(Seat{.seat = seat});
                            self.seats.append(ele);
                        }
                    } else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
                        const output = registry.bind(global.name, wl.Output, 3) catch return;

                        if (self.ally.create(ListItem(Output)) catch null) |ele| {
                            ele.init(Output{.output = output});
                            self.outputs.append(ele);
                        }
                    }
                },
                .global_remove => {},
            }
        }

        fn intoInit(self: *Self) !*Init {
            // free self at end of this function
            defer self.ally.destroy(self);

            const init = try self.ally.create(Init);
            // free allocated memory if not returning it
            errdefer self.ally.destroy(init);

            try init.init(self);

            return init;
        }
    };

    const Init = struct {
        const Self = @This();

        ally: std.mem.Allocator = undefined,

        running: bool = true,

        dpy: *wl.Display = undefined,
        registry: *wl.Registry = undefined,

        registry_listener: *Listener(wl.Registry, Self) = undefined,

        shm: *wl.Shm = undefined,
        compositor: *wl.Compositor = undefined,
        layer_shell: *zwlr.LayerShellV1 = undefined,
        xdg_output_manager: *zxdg.OutputManagerV1 = undefined,

        xkb_context: *xkb.xkb_context = undefined,

        cursor_theme: []const u8 = undefined,
        cursor_size: u32 = undefined,

        seats: *ListItem(Seat).Head() = undefined,
        outputs: *ListItem(Output).Head() = undefined,

        config: Input = undefined,

        fn init(self: *Self, uninit: *Uninit) !void {
            const xkb_context = xkb.xkb_context_new(xkb.XKB_CONTEXT_NO_FLAGS) orelse
                return error.NoXkbContext;
            // free xkb_context if we fail to initialize the state
            errdefer xkb.xkb_context_unref(xkb_context);

            const cursor_theme = std.process.getEnvVarOwned(uninit.ally, "XCURSOR_THEME") catch "";
            // only free cursor_theme if bailing
            errdefer uninit.ally.free(cursor_theme);

            const buf = try std.process.getEnvVarOwned(uninit.ally, "XCURSOR_SIZE");
            defer uninit.ally.free(buf);

            const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize;

            self.* = Self{
                .ally = uninit.ally,
                .dpy = uninit.dpy,
                .registry = uninit.registry,

                // these 4 fields are set by the registry listener so they may be null at this point
                // treat that as a hard error tho.
                .shm = uninit.shm orelse return error.NoWlShm,
                .compositor = uninit.compositor orelse return error.NoWlCompositor,
                .layer_shell = uninit.layer_shell orelse return error.NoWlrLayerShell,
                .xdg_output_manager = uninit.xdg_output_manager orelse return error.NoXdgOutputManager,

                // these 3 fields should never be null because init() will bail if these functions fail
                .xkb_context = xkb_context,
                .cursor_theme = cursor_theme,
                .cursor_size = cursor_size,

                .seats = uninit.seats,
                .outputs = uninit.outputs,

                .config = try Input.init(uninit.ally),
            };

            // i hope this is fine but appears to be requried to reset the listener on the registry
            self.registry_listener = uninit.registry_listener.cast(Self, self, Self.registryListener);

            // init seats

            var seats_iter = self.seats.safeIterator(.forward);
            while (seats_iter.next()) |item| {
                const seat = item.get();
                seat.init(self);
            }

            // init outputs

            var output_iter = self.outputs.safeIterator(.forward);
            while (output_iter.next()) |item| {
                const output = item.get();
                try output.init(self);
            }

            if (self.dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed;
        }

        fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void {
            _ = registry;
            _ = self;
            std.debug.print("registryListener called after init", .{});
            switch (event) {
                .global => {},
                .global_remove => {},
            }
        }

        fn getOptions(self: *Self) *Args {
            return &self.config.args.options;
        }

        fn stuff(self: *Self) !void {
            while (self.running) {
                if (self.dpy.dispatch() != .SUCCESS) return error.DispatchFailed;
            }
        }

        fn finish(self: *Self, box: Box) void {
            _ = self;
            _ = box;
        }

        fn removeOutput(self: *Self, output: *Output) void {
            const link = ListItem(Output).fromValue(output);
            link.remove();
            self.ally.destroy(link);
        }

        fn getOutputForSurface(self: *Self, surface: *wl.Surface) ?*Output {
            var output_iter = self.outputs.iterator(.forward);
            while (output_iter.next()) |next| {
                const output = next.get();
                if (output.surface == surface) {
                    return output;
                }
            }

            return null;
        }

        fn deinit(self: *Self) void {
            defer self.ally.destroy(self);

            var seats_iter = self.seats.safeIterator(.forward);
            while (seats_iter.next()) |next| {
                next.remove();
                self.ally.destroy(next);
            }
            self.ally.destroy(self.seats);


            var outputs_iter = self.outputs.safeIterator(.forward);
            while (outputs_iter.next()) |next| {
                next.remove();
                self.ally.destroy(next);
            }
            self.ally.destroy(self.outputs);

            self.shm.destroy();
            self.compositor.destroy();
            self.layer_shell.destroy();
            self.xdg_output_manager.destroy();
            xkb.xkb_context_unref(self.xkb_context);
            self.registry.destroy();
            self.ally.destroy(self.registry_listener);

            self.config.deinit();

            self.ally.free(self.cursor_theme);
        }
    };
};

const Args = struct {
    const Self = @This();

    help: bool = false,
    @"display-dimensions": bool = false,
    background: u32 = 0xFFFFFF40,
    border: u32 = 0x000000FF,
    selection: u32 = 0x00000000,
    choice: u32 = 0xFFFFFF40,
    format: ?[]const u8 = null,
    @"font-family": ?[]const u8 = null,
    @"border-weight": u32 = 2,
    @"single-point": bool = false,
    @"output-boxes": bool = false,
    @"restrict-selection": bool = false,
    // TODO: parse this in the format of W:H instead of a fraction
    @"aspect-ratio": ?f64 = null,

    pub const shorthands = .{
        .h = "help",
        .b = "background",
        .c = "border",
        .s = "selection",
        .B = "choice",
        .f = "format",
        .F = "font-family",
        .w = "border-weight",
        .p = "single-point",
        .o = "output-boxes",
        .r = "restrict-selection",
        .a = "aspect-ratio",
    };

    const usage = @embedFile("usage");

    const ResultType = zig_args.ParseArgsResult(Self, null);

    fn parse(ally: std.mem.Allocator) !ResultType {
        const self = try zig_args.parseForCurrentProcess(Self, ally, .print);
        // TODO: print help

        return self;
    }
};

const Input = struct {
    const Self = @This();

    args: Args.ResultType,
    in_boxes: ?Boxes = null,

    fn init(ally: std.mem.Allocator) !Self {
        const args = try Args.parse(ally);
        const in_boxes = try readInBoxes(ally);

        return Self {
            .in_boxes = in_boxes,
            .args = args,
        };
    }

    fn deinit(self: Self) void {
        self.args.deinit();
        if (self.in_boxes) |boxes| {
            boxes.deinit();
        }
    }

    fn readInBoxes(alloc: std.mem.Allocator) !?Boxes {
        var buf = [_]u8{0} ** 256;
        if (!std.io.getStdIn().isTty()) {
            var in_boxes = Boxes.init(alloc);
            errdefer in_boxes.deinit();

            var reader = std.io.getStdIn().reader();
            while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| {
                if (line.len == 0) continue;

                buf[line.len + 1] = 0;
                const sentineld = buf[0..line.len + 1:0];

                const result = try Box.parseFromStr(sentineld);
                defer result.destroy();

                try in_boxes.boxes.append(result.box);
            }

            return in_boxes;
        } else {
            return null;
        }
    }
};

pub fn run(ally: std.mem.Allocator) !void {
    // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
    std.debug.print("All your {s} are belong to us.\n", .{"codebase"});

    const state = try State.Uninit.create(ally);
    const init = try state.intoInit();
    defer init.deinit();

    try init.stuff();
}

pub fn main() !void {
    try run(std.heap.c_allocator);
}