const std = @import("std");
const c = @import("c.zig").c;
const config = @import("config.zig");
const client_mod = @import("client.zig");
const WM = client_mod.WM;
const Client = client_mod.Client;
const bar_mod = @import("bar.zig");
const util = @import("util.zig");

pub fn dispatch(wm: *WM, ev: *c.XEvent) void {
    switch (ev.type) {
        c.MapRequest => mapRequest(wm, &ev.xmaprequest),
        c.UnmapNotify => unmapNotify(wm, &ev.xunmap),
        c.DestroyNotify => destroyNotify(wm, &ev.xdestroywindow),
        c.ConfigureRequest => configureRequest(wm, &ev.xconfigurerequest),
        c.ClientMessage => clientMessage(wm, &ev.xclient),
        c.PropertyNotify => propertyNotify(wm, &ev.xproperty),
        c.ButtonPress => buttonPress(wm, &ev.xbutton),
        c.MotionNotify => motionNotify(wm, &ev.xmotion),
        c.ButtonRelease => buttonRelease(wm, &ev.xbutton),
        c.KeyPress => keyPress(wm, &ev.xkey),
        c.Expose => expose(wm, &ev.xexpose),
        c.FocusIn => focusIn(wm, &ev.xfocus),
        c.EnterNotify => enterNotify(wm, &ev.xcrossing),
        c.MappingNotify => mappingNotify(wm, &ev.xmapping),
        c.SelectionClear => selectionClear(wm),
        else => {},
    }
}

fn mapRequest(wm: *WM, ev: *c.XMapRequestEvent) void {
    client_mod.manage(wm, ev.window) catch |err| {
        util.log("manage: {}\n", .{err});
    };
    bar_mod.draw(wm);
}

fn unmapNotify(wm: *WM, ev: *c.XUnmapEvent) void {
    if (ev.event != wm.root) return;
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    if (cl.unmap_count > 0) {
        cl.unmap_count -= 1;
        return;
    }
    client_mod.unmanage(wm, cl);
    bar_mod.draw(wm);
}

fn destroyNotify(wm: *WM, ev: *c.XDestroyWindowEvent) void {
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    client_mod.unmanage(wm, cl);
    bar_mod.draw(wm);
}

fn configureRequest(wm: *WM, ev: *c.XConfigureRequestEvent) void {
    const cl = client_mod.listFind(wm.clients, ev.window);
    if (cl) |client| {
        if (client.flags.fullscreen) {
            client_mod.configure(wm, client);
            return;
        }
        if (ev.value_mask & c.CWX != 0) client.x = ev.x;
        if (ev.value_mask & c.CWY != 0) client.y = ev.y;
        if (ev.value_mask & c.CWWidth != 0) client.w = @intCast(ev.width);
        if (ev.value_mask & c.CWHeight != 0) client.h = @intCast(ev.height);
        if (ev.value_mask & c.CWBorderWidth != 0)
            client.bw = @intCast(ev.border_width);
        _ = c.XMoveResizeWindow(wm.display, client.window,
            client.x, client.y, client.w, client.h);
        client_mod.configure(wm, client);
        client_mod.updateFrameExtents(wm, client);
    } else {
        var changes: c.XWindowChanges = undefined;
        changes.x = ev.x;
        changes.y = ev.y;
        changes.width = ev.width;
        changes.height = ev.height;
        changes.border_width = ev.border_width;
        changes.sibling = ev.above;
        changes.stack_mode = ev.detail;
        _ = c.XConfigureWindow(wm.display, ev.window,
            @intCast(ev.value_mask), &changes);
    }
}

fn clientMessage(wm: *WM, ev: *c.XClientMessageEvent) void {
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    const a = wm.atoms;

    if (ev.message_type == a.net_wm_state) {
        const action = ev.data.l[0];
        const p1: c.Atom = @intCast(ev.data.l[1]);
        const p2: c.Atom = @intCast(ev.data.l[2]);
        const toggle = action == 2;
        const add = action == 1;
        const remove = action == 0;

        if (p1 == a.net_wm_state_fullscreen or p2 == a.net_wm_state_fullscreen) {
            const want = if (toggle) !cl.flags.fullscreen else add;
            client_mod.setFullscreen(wm, cl, want);
        }
        if (p1 == a.net_wm_state_hidden or p2 == a.net_wm_state_hidden) {
            if (add or (toggle and !cl.flags.minimized)) {
                client_mod.setMinimized(wm, cl, true);
                if (wm.focused == cl) client_mod.focusVisible(wm);
            } else if (remove or (toggle and cl.flags.minimized)) {
                client_mod.setMinimized(wm, cl, false);
                client_mod.focus(wm, cl);
            }
        }
        if (p1 == a.net_wm_state_demands_attention or p2 == a.net_wm_state_demands_attention) {
            if (remove or toggle) {
                cl.flags.urgent = if (toggle) !cl.flags.urgent else false;
            } else if (add) {
                cl.flags.urgent = true;
            }
            client_mod.setNetState(wm, cl);
        }
        if (p1 == a.net_wm_state_above or p2 == a.net_wm_state_above) {
            if (add) client_mod.raise(wm, cl);
        }
        bar_mod.draw(wm);
    } else if (ev.message_type == a.net_active_window) {
        if (cl.flags.minimized)
            client_mod.setMinimized(wm, cl, false);
        client_mod.focus(wm, cl);
        client_mod.raise(wm, cl);
        bar_mod.draw(wm);
    } else if (ev.message_type == a.net_close_window) {
        client_mod.closeClient(wm, cl);
    }
}

fn propertyNotify(wm: *WM, ev: *c.XPropertyEvent) void {
    if (ev.window == wm.root) {
        if (ev.atom == wm.atoms.net_wm_name or ev.atom == c.XA_WM_NAME) {
            bar_mod.draw(wm);
        }
        return;
    }
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    if (ev.state == c.PropertyDelete) return;

    if (ev.atom == c.XA_WM_NAME or ev.atom == wm.atoms.net_wm_name) {
        client_mod.readTitle(wm, cl);
        bar_mod.draw(wm);
    } else if (ev.atom == c.XA_WM_CLASS) {
        client_mod.readClass(wm, cl);
    } else if (ev.atom == c.XA_WM_HINTS) {
        const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window);
        if (wm_hints != null) {
            const old_urgent = cl.flags.urgent;
            cl.flags.urgent = (wm_hints.*.flags & c.XUrgencyHint) != 0;
            if (wm_hints.*.flags & c.InputHint != 0)
                cl.input_hint = wm_hints.*.input != 0;
            cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus;
            client_mod.setNetState(wm, cl);
            if (old_urgent != cl.flags.urgent)
                bar_mod.draw(wm);
            _ = c.XFree(wm_hints);
        }
    }
}

fn buttonPress(wm: *WM, ev: *c.XButtonEvent) void {
    if (ev.window == wm.bar.window) {
        barClick(wm, ev);
        return;
    }

    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;

    if (ev.state & c.Mod1Mask != 0 and !cl.flags.fullscreen) {
        if (wm.focused != cl) {
            client_mod.focus(wm, cl);
            client_mod.raise(wm, cl);
            bar_mod.draw(wm);
        }
        const resizing = ev.button == 3;
        wm.grab = .{
            .client = cl,
            .orig_x = cl.x,
            .orig_y = cl.y,
            .orig_w = cl.w,
            .orig_h = cl.h,
            .start_x = ev.x_root,
            .start_y = ev.y_root,
            .resizing = resizing,
        };
        _ = c.XGrabPointer(wm.display, wm.root, 0,
            c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
            c.GrabModeAsync, c.GrabModeAsync, 0, 0, c.CurrentTime);
        return;
    }

    if (wm.focused != cl) {
        client_mod.focus(wm, cl);
        client_mod.raise(wm, cl);
        bar_mod.draw(wm);
    }
    _ = c.XAllowEvents(wm.display, c.ReplayPointer, c.CurrentTime);
}

fn barClick(wm: *WM, ev: *c.XButtonEvent) void {
    const cl = client_mod.findClientByBarClick(wm, ev.x) orelse return;
    switch (ev.button) {
        c.Button1 => {
            if (cl.flags.minimized)
                client_mod.setMinimized(wm, cl, false);
            client_mod.focus(wm, cl);
            client_mod.raise(wm, cl);
            bar_mod.draw(wm);
        },
        c.Button2 => {
            client_mod.closeClient(wm, cl);
        },
        c.Button3 => {
            client_mod.setMinimized(wm, cl, !cl.flags.minimized);
            if (cl.flags.minimized and wm.focused == cl) {
                client_mod.focusVisible(wm);
            }
            bar_mod.draw(wm);
        },
        else => {},
    }
}

fn motionNotify(wm: *WM, ev: *c.XMotionEvent) void {
    const grab = wm.grab orelse return;
    const cl = grab.client;

    var dummy: c.XEvent = undefined;
    while (c.XCheckMaskEvent(wm.display, c.PointerMotionMask, &dummy) != 0) {
        ev.x_root = dummy.xmotion.x_root;
        ev.y_root = dummy.xmotion.y_root;
    }

    const dx = ev.x_root - grab.start_x;
    const dy = ev.y_root - grab.start_y;

    if (grab.resizing) {
        var nw: i32 = @as(i32, @intCast(grab.orig_w)) + dx;
        var nh: i32 = @as(i32, @intCast(grab.orig_h)) + dy;
        if (nw < 1) nw = 1;
        if (nh < 1) nh = 1;
        const s = client_mod.applySizeHints(cl, nw, nh);
        nw = @max(s.w, 1);
        nh = @max(s.h, 1);
        client_mod.resize(wm, cl, cl.x, cl.y, @intCast(nw), @intCast(nh));
    } else {
        var nx = grab.orig_x + dx;
        var ny = grab.orig_y + dy;
        nx = applySnap(nx, @intCast(cl.w), @intCast(wm.sw));
        ny = applySnap(ny, @intCast(cl.h), @intCast(wm.sh));
        cl.x = nx;
        cl.y = ny;
        _ = c.XMoveWindow(wm.display, cl.window, nx, ny);
        client_mod.configure(wm, cl);
    }
}

fn applySnap(pos: i32, size: i32, limit: i32) i32 {
    if (@abs(pos) < config.snap_distance) return 0;
    if (@abs(pos + size - limit) < config.snap_distance) return limit - size;
    return pos;
}

fn buttonRelease(wm: *WM, ev: *c.XButtonEvent) void {
    _ = ev;
    if (wm.grab != null) {
        wm.grab = null;
        _ = c.XUngrabPointer(wm.display, c.CurrentTime);
    }
}

fn keyPress(wm: *WM, ev: *c.XKeyEvent) void {
    const keysym = c.XkbKeycodeToKeysym(wm.display, @intCast(ev.keycode), 0, 0);
    for (&config.keybinds) |kb| {
        if (kb.keysym != keysym) continue;
        if (kb.mod != (ev.state & ~@as(c_uint, c.LockMask))) continue;

        switch (kb.action) {
            .quit => wm.running = false,
            .close_focused => {
                if (wm.focused) |cl| client_mod.closeClient(wm, cl);
            },
            .toggle_fullscreen => {
                if (wm.focused) |cl| {
                    client_mod.setFullscreen(wm, cl, !cl.flags.fullscreen);
                    bar_mod.draw(wm);
                }
            },
            .focus_next => client_mod.focusNext(wm),
            .minimize => {
                if (wm.focused) |cl| {
                    client_mod.setMinimized(wm, cl, true);
                    client_mod.focusVisible(wm);
                    bar_mod.draw(wm);
                }
            },
            .spawn => |cmd| util.spawn(wm.allocator, cmd),
        }
        return;
    }
}

fn expose(wm: *WM, ev: *c.XExposeEvent) void {
    if (ev.window == wm.bar.window and ev.count == 0)
        bar_mod.draw(wm);
}

fn focusIn(wm: *WM, ev: *c.XFocusChangeEvent) void {
    if (ev.detail == c.NotifyInferior or ev.detail == c.NotifyPointer) return;
    if (ev.window == wm.root) return;
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    if (wm.focused != cl) {
        client_mod.focus(wm, cl);
        bar_mod.draw(wm);
    }
}

fn enterNotify(wm: *WM, ev: *c.XCrossingEvent) void {
    if (ev.detail == c.NotifyInferior or ev.mode == c.NotifyGrab or ev.mode == c.NotifyUngrab) return;
    if (!config.focus_follows_mouse) return;
    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
    if (cl.flags.never_focus) return;
    client_mod.focus(wm, cl);
    bar_mod.draw(wm);
}

fn mappingNotify(wm: *WM, ev: *c.XMappingEvent) void {
    _ = c.XRefreshKeyboardMapping(ev);
    if (ev.request == c.MappingKeyboard)
        grabKeys(wm);
}

fn selectionClear(wm: *WM) void {
    wm.running = false;
}

pub fn grabKeys(wm: *WM) void {
    _ = c.XUngrabKey(wm.display, c.AnyKey, c.AnyModifier, wm.root);
    for (&config.keybinds) |kb| {
        const code = c.XKeysymToKeycode(wm.display, kb.keysym);
        _ = c.XGrabKey(wm.display, code, kb.mod, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
        _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
        _ = c.XGrabKey(wm.display, code, kb.mod | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
        _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
    }
}
