const std = @import("std");
const c = @import("c.zig").c;
const config = @import("config.zig");
const atoms_mod = @import("atoms.zig");
const Atoms = atoms_mod.Atoms;

pub const Client = struct {
    window: c.Window,
    x: i32 = 0,
    y: i32 = 0,
    w: u32 = 0,
    h: u32 = 0,
    bw: u32 = config.border_width,
    saved: struct {
        x: i32 = 0,
        y: i32 = 0,
        w: u32 = 0,
        h: u32 = 0,
    } = .{},
    min_w: i32 = 0,
    min_h: i32 = 0,
    max_w: i32 = 0,
    max_h: i32 = 0,
    base_w: i32 = 0,
    base_h: i32 = 0,
    inc_w: i32 = 0,
    inc_h: i32 = 0,
    min_aspect: f64 = 0,
    max_aspect: f64 = 0,
    has_aspect: bool = false,
    flags: packed struct {
        floating: bool = false,
        fullscreen: bool = false,
        minimized: bool = false,
        urgent: bool = false,
        never_focus: bool = false,
        fixed_size: bool = false,
    } = .{},
    title: []u8 = &.{},
    class: []u8 = &.{},
    prev: ?*Client = null,
    next: ?*Client = null,
    unmap_count: u32 = 0,
    supports_delete: bool = false,
    supports_take_focus: bool = false,
    input_hint: bool = true,
    transient_for: c.Window = 0,
};

pub const GrabState = struct {
    client: *Client,
    orig_x: i32,
    orig_y: i32,
    orig_w: u32,
    orig_h: u32,
    start_x: i32,
    start_y: i32,
    resizing: bool,
};

pub const ButtonEntry = struct {
    x: i32,
    w: u32,
    win: c.Window,
};

pub const Bar = struct {
    window: c.Window = 0,
    pixmap: c.Pixmap = 0,
    draw: ?*c.XftDraw = null,
    fonts: [config.font_names.len]?*c.XftFont,
    colors: struct {
        bg: c.XftColor,
        fg: c.XftColor,
        sel_bg: c.XftColor,
        min_fg: c.XftColor,
        urg_fg: c.XftColor,
        border_focused: c.XftColor,
        border_unfocused: c.XftColor,
    },
    buttons: [128]ButtonEntry,
    button_count: usize = 0,
};

pub const WM = struct {
    display: ?*c.Display,
    screen: c_int,
    root: c.Window,
    sw: u32,
    sh: u32,
    atoms: Atoms,
    bar: Bar,
    clients: ?*Client,
    focused: ?*Client,
    running: bool,
    allocator: std.mem.Allocator,
    support_win: c.Window,
    grab: ?GrabState,
    visual: ?*c.Visual,
    colormap: c.Colormap,
};

pub fn listAppend(head: *?*Client, cl: *Client) void {
    cl.next = null;
    cl.prev = null;
    if (head.*) |h| {
        var it: *Client = h;
        while (it.next) |next| it = next;
        it.next = cl;
        cl.prev = it;
    } else {
        head.* = cl;
    }
}

pub fn listRemove(head: *?*Client, cl: *Client) void {
    if (cl.prev) |p| p.next = cl.next else head.* = cl.next;
    if (cl.next) |n| n.prev = cl.prev;
    cl.prev = null;
    cl.next = null;
}

pub fn listFind(head: ?*Client, win: c.Window) ?*Client {
    var it = head;
    while (it) |cl| : (it = cl.next) {
        if (cl.window == win) return cl;
    }
    return null;
}

pub fn listCount(head: ?*Client) usize {
    var n: usize = 0;
    var it = head;
    while (it) |cl| : (it = cl.next) n += 1;
    return n;
}

pub fn listLast(head: ?*Client) ?*Client {
    var it = head orelse return null;
    while (it.next) |next| it = next;
    return it;
}

fn setWmState(wm: *WM, cl: *Client, state: i32) void {
    const data = [2]c_long{ @intCast(state), 0 };
    _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.wm_state,
        wm.atoms.wm_state, 32, c.PropModeReplace, @ptrCast(&data), 2);
}

fn sendProtocol(wm: *WM, cl: *Client, protocol: c.Atom) void {
    var ev: c.XEvent = undefined;
    @memset(@as([*]u8, @ptrCast(&ev))[0..@sizeOf(c.XEvent)], 0);
    ev.type = c.ClientMessage;
    ev.xclient.window = cl.window;
    ev.xclient.message_type = wm.atoms.wm_protocols;
    ev.xclient.format = 32;
    ev.xclient.data.l[0] = @intCast(protocol);
    ev.xclient.data.l[1] = @intCast(c.CurrentTime);
    _ = c.XSendEvent(wm.display, cl.window, 0, c.NoEventMask, &ev);
}

fn updateFocusPolicy(cl: *Client) void {
    cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus;
}

pub fn closeClient(wm: *WM, cl: *Client) void {
    if (cl.supports_delete) {
        sendProtocol(wm, cl, wm.atoms.wm_delete_window);
    } else {
        _ = c.XKillClient(wm.display, cl.window);
    }
}

pub fn readTitle(wm: *WM, cl: *Client) void {
    if (cl.title.len > 0) {
        wm.allocator.free(cl.title);
        cl.title = &.{};
    }
    var text: c.XTextProperty = undefined;
    if (c.XGetTextProperty(wm.display, cl.window, &text, wm.atoms.net_wm_name) != 0) {
        if (text.value != null) {
            defer _ = c.XFree(text.value);
            if (text.nitems > 0) {
                var len: usize = @intCast(text.nitems);
                if (text.value[len - 1] == 0) len -= 1;
                cl.title = wm.allocator.dupe(u8, text.value[0..len]) catch &.{};
                return;
            }
        }
    }
    var name: [*c]u8 = null;
    if (c.XFetchName(wm.display, cl.window, &name) != 0) {
        if (name != null) {
            cl.title = wm.allocator.dupe(u8, std.mem.sliceTo(name.?, 0)) catch &.{};
            _ = c.XFree(name);
        }
    }
}

pub fn readClass(wm: *WM, cl: *Client) void {
    if (cl.class.len > 0) {
        wm.allocator.free(cl.class);
        cl.class = &.{};
    }
    var hint: c.XClassHint = .{ .res_name = null, .res_class = null };
    if (c.XGetClassHint(wm.display, cl.window, &hint) != 0) {
        if (hint.res_class != null) {
            cl.class = wm.allocator.dupe(u8, std.mem.sliceTo(hint.res_class.?, 0)) catch &.{};
            _ = c.XFree(hint.res_class);
        }
        if (hint.res_name != null) _ = c.XFree(hint.res_name);
    }
}

fn readProtocols(wm: *WM, cl: *Client) void {
    var protos: [*c]c.Atom = null;
    var n: c_int = 0;
    if (c.XGetWMProtocols(wm.display, cl.window, &protos, &n) != 0) {
        var i: c_int = 0;
        while (i < n) : (i += 1) {
            if (protos[@intCast(i)] == wm.atoms.wm_delete_window)
                cl.supports_delete = true;
            if (protos[@intCast(i)] == wm.atoms.wm_take_focus)
                cl.supports_take_focus = true;
        }
        _ = c.XFree(protos);
    }
    updateFocusPolicy(cl);
}

fn readHints(wm: *WM, cl: *Client) void {
    var hints: c.XSizeHints = undefined;
    var supplied: c_long = 0;
    if (c.XGetWMNormalHints(wm.display, cl.window, &hints, &supplied) != 0) {
        if (hints.flags & c.PMinSize != 0) {
            cl.min_w = hints.min_width;
            cl.min_h = hints.min_height;
        }
        if (hints.flags & c.PMaxSize != 0) {
            cl.max_w = hints.max_width;
            cl.max_h = hints.max_height;
        }
        if (hints.flags & c.PBaseSize != 0) {
            cl.base_w = hints.base_width;
            cl.base_h = hints.base_height;
        }
        if (hints.flags & c.PResizeInc != 0) {
            cl.inc_w = hints.width_inc;
            cl.inc_h = hints.height_inc;
        }
        if (hints.flags & c.PAspect != 0) {
            if (hints.min_aspect.y > 0 and hints.max_aspect.y > 0) {
                cl.min_aspect = @as(f64, @floatFromInt(hints.min_aspect.x)) /
                    @as(f64, @floatFromInt(hints.min_aspect.y));
                cl.max_aspect = @as(f64, @floatFromInt(hints.max_aspect.x)) /
                    @as(f64, @floatFromInt(hints.max_aspect.y));
                cl.has_aspect = true;
            }
        }
        if (hints.flags & (c.PMinSize | c.PBaseSize) != 0 and
            hints.flags & c.PMaxSize != 0 and
            hints.min_width == hints.max_width and
            hints.min_height == hints.max_height)
        {
            cl.flags.fixed_size = true;
            cl.flags.floating = true;
        }
    }

    const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window);
    if (wm_hints != null) {
        if (wm_hints.*.flags & c.InputHint != 0)
            cl.input_hint = wm_hints.*.input != 0;
        if (wm_hints.*.flags & c.XUrgencyHint != 0)
            cl.flags.urgent = true;
        _ = c.XFree(wm_hints);
    }
    updateFocusPolicy(cl);

    var trans: c.Window = 0;
    _ = c.XGetTransientForHint(wm.display, cl.window, &trans);
    if (trans != 0) {
        cl.transient_for = trans;
        cl.flags.floating = true;
    }
}

fn checkWindowType(wm: *WM, cl: *Client) void {
    var actual_type: c.Atom = 0;
    var actual_format: c_int = 0;
    var nitems: c_ulong = 0;
    var bytes_after: c_ulong = 0;
    var prop: [*c]u8 = null;
    if (c.XGetWindowProperty(wm.display, cl.window, wm.atoms.net_wm_window_type,
        0, 1024, 0, c.XA_ATOM, &actual_type, &actual_format,
        &nitems, &bytes_after, &prop) == c.Success and prop != null)
    {
        const atoms: [*]c.Atom = @ptrCast(@alignCast(prop));
        var i: usize = 0;
        while (i < nitems) : (i += 1) {
            const a = atoms[i];
            if (a == wm.atoms.net_wm_window_type_dialog or
                a == wm.atoms.net_wm_window_type_utility or
                a == wm.atoms.net_wm_window_type_toolbar or
                a == wm.atoms.net_wm_window_type_splash or
                a == wm.atoms.net_wm_window_type_menu or
                a == wm.atoms.net_wm_window_type_dock)
            {
                cl.flags.floating = true;
            }
        }
        _ = c.XFree(prop);
    }
}

fn applyRules(cl: *Client) void {
    for (&config.rules) |rule| {
        if (rule.class) |rc| {
            if (cl.class.len > 0 and std.mem.indexOf(u8, cl.class, rc) != null) {
                cl.flags.floating = rule.floating;
            }
        }
    }
}

pub fn applySizeHints(cl: *Client, w: i32, h: i32) struct { w: i32, h: i32 } {
    var nw = w;
    var nh = h;
    if (cl.inc_w > 0 and cl.base_w >= 0)
        nw = cl.base_w + cl.inc_w * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nw - cl.base_w)) / @as(f64, @floatFromInt(cl.inc_w)))));
    if (cl.inc_h > 0 and cl.base_h >= 0)
        nh = cl.base_h + cl.inc_h * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nh - cl.base_h)) / @as(f64, @floatFromInt(cl.inc_h)))));
    if (cl.min_w > 0 and nw < cl.min_w) nw = cl.min_w;
    if (cl.min_h > 0 and nh < cl.min_h) nh = cl.min_h;
    if (cl.max_w > 0 and nw > cl.max_w) nw = cl.max_w;
    if (cl.max_h > 0 and nh > cl.max_h) nh = cl.max_h;
    if (cl.has_aspect) {
        const aspect = @as(f64, @floatFromInt(nw)) / @as(f64, @floatFromInt(@max(nh, 1)));
        if (aspect < cl.min_aspect)
            nw = @intFromFloat(@as(f64, @floatFromInt(nh)) * cl.min_aspect);
        if (aspect > cl.max_aspect)
            nh = @intFromFloat(@as(f64, @floatFromInt(nw)) / cl.max_aspect);
    }
    if (nw < 1) nw = 1;
    if (nh < 1) nh = 1;
    return .{ .w = nw, .h = nh };
}

pub fn configure(wm: *WM, cl: *Client) void {
    var ce: c.XConfigureEvent = undefined;
    ce.type = c.ConfigureNotify;
    ce.display = wm.display;
    ce.event = cl.window;
    ce.window = cl.window;
    ce.x = cl.x;
    ce.y = cl.y;
    ce.width = @intCast(cl.w);
    ce.height = @intCast(cl.h);
    ce.border_width = @intCast(cl.bw);
    ce.above = 0;
    ce.override_redirect = 0;
    _ = c.XSendEvent(wm.display, cl.window, 0, c.StructureNotifyMask, @ptrCast(&ce));
}

pub fn resize(wm: *WM, cl: *Client, x: i32, y: i32, w: u32, h: u32) void {
    const s = applySizeHints(cl, @intCast(w), @intCast(h));
    const sw: u32 = @intCast(@max(s.w, 1));
    const sh: u32 = @intCast(@max(s.h, 1));
    cl.x = x;
    cl.y = y;
    cl.w = sw;
    cl.h = sh;
    _ = c.XMoveResizeWindow(wm.display, cl.window, x, y, sw, sh);
    configure(wm, cl);
    updateFrameExtents(wm, cl);
}

pub fn setFullscreen(wm: *WM, cl: *Client, fullscreen: bool) void {
    const was_fs = cl.flags.fullscreen;
    cl.flags.fullscreen = fullscreen;

    if (fullscreen and !was_fs) {
        cl.saved = .{ .x = cl.x, .y = cl.y, .w = cl.w, .h = cl.h };
        cl.bw = 0;
        _ = c.XSetWindowBorderWidth(wm.display, cl.window, 0);
        resize(wm, cl, 0, @intCast(config.bar_height), wm.sw, wm.sh - config.bar_height);
        _ = c.XRaiseWindow(wm.display, cl.window);
        setNetState(wm, cl);
    } else if (!fullscreen and was_fs) {
        cl.bw = config.border_width;
        _ = c.XSetWindowBorderWidth(wm.display, cl.window, @intCast(config.border_width));
        resize(wm, cl, cl.saved.x, cl.saved.y, cl.saved.w, cl.saved.h);
        setNetState(wm, cl);
    }
}

pub fn setMinimized(wm: *WM, cl: *Client, minimized: bool) void {
    const was_min = cl.flags.minimized;
    cl.flags.minimized = minimized;
    if (minimized and !was_min) {
        cl.unmap_count += 1;
        _ = c.XUnmapWindow(wm.display, cl.window);
        setWmState(wm, cl, 3);
        setNetState(wm, cl);
    } else if (!minimized and was_min) {
        _ = c.XMapWindow(wm.display, cl.window);
        setWmState(wm, cl, 1);
        setNetState(wm, cl);
    }
}

pub fn setNetState(wm: *WM, cl: *Client) void {
    var atoms: [4]c.Atom = undefined;
    var n: usize = 0;
    if (cl.flags.fullscreen) {
        atoms[n] = wm.atoms.net_wm_state_fullscreen;
        n += 1;
    }
    if (cl.flags.minimized) {
        atoms[n] = wm.atoms.net_wm_state_hidden;
        n += 1;
    }
    if (cl.flags.urgent) {
        atoms[n] = wm.atoms.net_wm_state_demands_attention;
        n += 1;
    }
    if (n > 0) {
        _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_wm_state,
            c.XA_ATOM, 32, c.PropModeReplace, @ptrCast(&atoms), @intCast(n));
    } else {
        _ = c.XDeleteProperty(wm.display, cl.window, wm.atoms.net_wm_state);
    }
}

pub fn grabClientButtons(wm: *WM, win: c.Window, focused: bool) void {
    _ = c.XUngrabButton(wm.display, c.AnyButton, c.AnyModifier, win);

    _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask, win, 0,
        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
    _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask, win, 0,
        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
    _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask | c.LockMask, win, 0,
        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
    _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask | c.LockMask, win, 0,
        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
        c.GrabModeAsync, c.GrabModeAsync, 0, 0);

    if (!focused) {
        _ = c.XGrabButton(wm.display, c.AnyButton, c.AnyModifier, win, 0,
            c.ButtonPressMask, c.GrabModeSync, c.GrabModeSync, 0, 0);
    }
}

pub fn focus(wm: *WM, cl: ?*Client) void {
    if (wm.focused) |prev| {
        setBorder(wm, prev, false);
        grabClientButtons(wm, prev.window, false);
    }
    wm.focused = cl;
    if (cl) |client| {
        setBorder(wm, client, true);
        grabClientButtons(wm, client.window, true);
        _ = c.XRaiseWindow(wm.display, client.window);
        if (!client.flags.never_focus) {
            if (client.input_hint) {
                _ = c.XSetInputFocus(wm.display, client.window, c.RevertToPointerRoot, c.CurrentTime);
            }
            if (client.supports_take_focus) {
                sendProtocol(wm, client, wm.atoms.wm_take_focus);
            }
        }
        _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_active_window,
            c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&client.window), 1);
    } else {
        _ = c.XSetInputFocus(wm.display, wm.root, c.RevertToPointerRoot, c.CurrentTime);
        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_active_window);
    }
}

fn setBorder(wm: *WM, cl: *Client, focused: bool) void {
    const color = if (focused)
        wm.bar.colors.border_focused.pixel
    else
        wm.bar.colors.border_unfocused.pixel;
    _ = c.XSetWindowBorder(wm.display, cl.window, color);
}

pub fn focusNext(wm: *WM) void {
    const start = wm.focused orelse wm.clients orelse return;
    var it = start;
    while (true) {
        it = it.next orelse wm.clients orelse return;
        if (!it.flags.minimized) {
            focus(wm, it);
            _ = c.XRaiseWindow(wm.display, it.window);
            return;
        }
        if (it == start) break;
    }
}

pub fn focusVisible(wm: *WM) void {
    var it = wm.clients;
    while (it) |cl| : (it = cl.next) {
        if (!cl.flags.minimized) {
            focus(wm, cl);
            return;
        }
    }
    focus(wm, null);
}

pub fn raise(wm: *WM, cl: *Client) void {
    _ = c.XRaiseWindow(wm.display, cl.window);
    listRemove(&wm.clients, cl);
    listAppend(&wm.clients, cl);
    updateClientList(wm);
}

pub fn updateClientList(wm: *WM) void {
    const n = listCount(wm.clients);
    if (n == 0) {
        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list);
        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking);
        return;
    }
    var buf: [256]c.Window = undefined;
    var i: usize = 0;
    var it = wm.clients;
    while (it) |cl| : (it = cl.next) {
        if (i < buf.len) {
            buf[i] = cl.window;
            i += 1;
        }
    }
    _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list,
        c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));

    i = 0;
    it = wm.clients;
    while (it) |cl| : (it = cl.next) {
        if (i < buf.len) {
            buf[i] = cl.window;
            i += 1;
        }
    }
    _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking,
        c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));
}

pub fn updateFrameExtents(wm: *WM, cl: *Client) void {
    const data = [4]c_long{
        @intCast(cl.bw),
        @intCast(cl.bw),
        @intCast(cl.bw),
        @intCast(cl.bw),
    };
    _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_frame_extents,
        c.XA_CARDINAL, 32, c.PropModeReplace, @ptrCast(&data), 4);
}

pub fn manage(wm: *WM, window: c.Window) !void {
    if (listFind(wm.clients, window) != null) return;
    if (window == wm.bar.window) return;

    var wa: c.XWindowAttributes = undefined;
    if (c.XGetWindowAttributes(wm.display, window, &wa) == 0) return;
    if (wa.override_redirect != 0) return;

    const cl = try wm.allocator.create(Client);
    errdefer wm.allocator.destroy(cl);
    cl.* = .{
        .window = window,
        .x = wa.x,
        .y = wa.y,
        .w = @intCast(wa.width),
        .h = @intCast(wa.height),
        .bw = config.border_width,
    };

    readHints(wm, cl);
    readTitle(wm, cl);
    readClass(wm, cl);
    readProtocols(wm, cl);
    checkWindowType(wm, cl);
    applyRules(cl);

    if (!cl.flags.floating) {
        cl.y = @max(cl.y, @as(i32, @intCast(config.bar_height)));
    }

    listAppend(&wm.clients, cl);
    setWmState(wm, cl, 1);

    _ = c.XSelectInput(wm.display, window,
        c.StructureNotifyMask | c.PropertyChangeMask |
        c.EnterWindowMask | c.FocusChangeMask);

    _ = c.XSetWindowBorderWidth(wm.display, window, @intCast(config.border_width));
    _ = c.XMoveResizeWindow(wm.display, window, cl.x, cl.y, cl.w, cl.h);
    _ = c.XMapWindow(wm.display, window);

    grabClientButtons(wm, window, false);
    updateFrameExtents(wm, cl);
    focus(wm, cl);
    updateClientList(wm);
}

pub fn unmanage(wm: *WM, cl: *Client) void {
    if (wm.focused == cl) {
        const next = cl.next orelse cl.prev;
        focus(wm, next);
    }
    listRemove(&wm.clients, cl);
    setWmState(wm, cl, 0);

    if (cl.title.len > 0) wm.allocator.free(cl.title);
    if (cl.class.len > 0) wm.allocator.free(cl.class);
    wm.allocator.destroy(cl);

    updateClientList(wm);
}

pub fn findClientByBarClick(wm: *WM, x: i32) ?*Client {
    var i: usize = 0;
    while (i < wm.bar.button_count) : (i += 1) {
        const btn = wm.bar.buttons[i];
        if (x >= btn.x and x < btn.x + @as(i32, @intCast(btn.w))) {
            return listFind(wm.clients, btn.win);
        }
    }
    return null;
}
