const std = @import("std");
const xev = @import("xev");
const wayland = @import("wayland");

const c = @import("ffi.zig").c;
const options = @import("options.zig");

const DrawTimerHandler = @import("DrawTimerHandler.zig");
const Gfx = @import("Gfx.zig");
const Globals = @import("Globals.zig");
const OutputInfo = @import("OutputInfo.zig");
const OutputWindow = @import("OutputWindow.zig");
const PointerState = @import("PointerState.zig");

const wl = wayland.client.wl;
const zwlr = wayland.client.zwlr;
const zxdg = wayland.client.zxdg;

pub const std_options = std.Options{
    .log_level = .debug,
};

pub fn main() !void {
    std.log.info("initializing event loop", .{});
    var loop = try xev.Loop.init(.{});
    defer loop.deinit();

    std.log.info("connecting to wayland display", .{});
    const dpy = try wl.Display.connect(null);
    defer dpy.disconnect();
    const globs = try Globals.collect(dpy);
    defer globs.outputs.deinit();

    const output_info = try std.heap.c_allocator.alloc(OutputInfo, globs.outputs.items.len);
    defer std.heap.c_allocator.free(output_info);
    @memset(output_info, .{});

    for (globs.outputs.items, 0..) |output, i| {
        const xdg_output = try globs.xdg_output_manager.getXdgOutput(output);
        xdg_output.setListener(*OutputInfo, xdgOutputListener, &output_info[i]);
    }

    if (dpy.roundtrip() != .SUCCESS) return error.RoundtipFail;

    if (c.eglBindAPI(c.EGL_OPENGL_API) == 0) return error.EGLError;

    const egl_dpy = c.eglGetDisplay(@ptrCast(dpy)) orelse return error.EGLError;
    if (c.eglInitialize(egl_dpy, null, null) != c.EGL_TRUE) return error.EGLError;
    defer _ = c.eglTerminate(egl_dpy);

    const config = egl_conf: {
        var config: c.EGLConfig = undefined;
        var n_config: i32 = 0;
        if (c.eglChooseConfig(
            egl_dpy,
            &[_]i32{
                c.EGL_SURFACE_TYPE,    c.EGL_WINDOW_BIT,
                c.EGL_RENDERABLE_TYPE, c.EGL_OPENGL_BIT,
                c.EGL_RED_SIZE,        8,
                c.EGL_GREEN_SIZE,      8,
                c.EGL_BLUE_SIZE,       8,
                c.EGL_NONE,
            },
            &config,
            1,
            &n_config,
        ) != c.EGL_TRUE) return error.EGLError;
        break :egl_conf config;
    };

    std.log.info("creating EGL context", .{});
    const egl_ctx = c.eglCreateContext(
        egl_dpy,
        config,
        c.EGL_NO_CONTEXT,
        &[_]i32{
            c.EGL_CONTEXT_MAJOR_VERSION,       4,
            c.EGL_CONTEXT_MINOR_VERSION,       3,
            c.EGL_CONTEXT_OPENGL_DEBUG,        c.EGL_TRUE,
            c.EGL_CONTEXT_OPENGL_PROFILE_MASK, c.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
            c.EGL_NONE,
        },
    ) orelse return error.EGLError;
    defer _ = c.eglDestroyContext(egl_dpy, egl_ctx);

    const output_windows = try std.heap.c_allocator.alloc(OutputWindow, globs.outputs.items.len);
    defer std.heap.c_allocator.free(output_windows);

    for (output_windows, 0..) |*output_window, i| {
        std.log.info("creating EGL surface #{}", .{i});
        const surface = try globs.compositor.createSurface();

        const lsurf = try globs.layer_shell.getLayerSurface(
            surface,
            globs.outputs.items[i],
            .background,
            "wlbg",
        );

        var winsize: ?[2]c_int = null;
        lsurf.setListener(*?[2]c_int, layerSurfaceListener, &winsize);

        lsurf.setAnchor(.{
            .top = true,
            .right = true,
            .bottom = true,
            .left = true,
        });
        lsurf.setExclusiveZone(-1);

        surface.commit();

        if (dpy.dispatch() != .SUCCESS) return error.DispatchFail;

        const egl_win = win: {
            std.log.info("creating EGL window #{}", .{i});
            const size = winsize orelse return error.DidNotGetWindowSize;
            break :win try wl.EglWindow.create(surface, size[0], size[1]);
        };
        errdefer egl_win.destroy();

        const egl_surface = c.eglCreateWindowSurface(
            egl_dpy,
            config,
            @ptrCast(egl_win),
            null,
        ) orelse return error.EGLError;
        errdefer _ = c.eglDestroySurface(egl_dpy, egl_surface);

        output_window.* = .{
            .egl_win = egl_win,
            .egl_surface = egl_surface,
            .surface = surface,
        };
    }
    defer for (output_windows) |output| output.deinit(egl_ctx);

    var r_timer_completion: xev.Completion = undefined;
    var r_timer = try xev.Timer.init();
    defer r_timer.deinit();

    var dth = DrawTimerHandler{
        .should_redraw = try std.heap.c_allocator.alloc(bool, output_info.len),
        .completion = &r_timer_completion,
        .loop = &loop,
    };
    defer std.heap.c_allocator.free(dth.should_redraw);
    @memset(dth.should_redraw, true);

    var pointer_state = PointerState{
        .active_surface_idx = null,
        .surface_positions = try std.heap.c_allocator.alloc([2]c_int, output_info.len),
    };
    defer std.heap.c_allocator.free(pointer_state.surface_positions);
    @memset(pointer_state.surface_positions, .{ 0, 0 });

    var pointer_listener_data = PointerListenerData{
        .pstate = &pointer_state,
        .outputs = output_windows,
        .dth = &dth,
    };

    const pointer = try globs.seat.getPointer();
    defer pointer.destroy();
    pointer.setListener(*PointerListenerData, pointerListener, &pointer_listener_data);

    const base_offset: [2]i32 = off: {
        if (comptime options.multihead_mode == .individual) break :off .{ 0, 0 };

        var total_width: i32 = 0;
        var total_height: i32 = 0;
        for (output_info) |inf| {
            const xmax = inf.x;
            const ymax = inf.y;

            if (xmax > total_width)
                total_width = xmax;
            if (ymax > total_height)
                total_height = ymax;
        }

        break :off .{
            @divTrunc(total_width, 2),
            @divTrunc(total_height, 2),
        };
    };

    if (c.eglMakeCurrent(
        egl_dpy,
        output_windows[0].egl_surface,
        output_windows[0].egl_surface,
        egl_ctx,
    ) != c.EGL_TRUE) return error.EGLError;

    c.glDebugMessageCallback(&glDebugCb, null);
    c.glEnable(c.GL_DEBUG_OUTPUT);
    std.log.info("initialized OpenGL {s}", .{c.glGetString(c.GL_VERSION)});

    var gfx = try Gfx.init(dpy, egl_dpy, output_info);
    defer gfx.deinit();

    var rdata = RenderData{
        .gfx = &gfx,
        .egl_dpy = egl_dpy,
        .egl_ctx = egl_ctx,
        .outputs = output_windows,
        .output_info = output_info,
        .last_time = loop.now(),
        .base_offset = base_offset,
        .pointer_state = &pointer_state,
        .dth = &dth,
    };

    var rbg_timer_completion: xev.Completion = undefined;
    var rbg_timer = try xev.Timer.init();
    defer rbg_timer.deinit();

    rbg_timer.run(
        &loop,
        &rbg_timer_completion,
        0,
        RenderData,
        &rdata,
        renderBackgroundCb,
    );

    r_timer.run(
        &loop,
        &r_timer_completion,
        0,
        RenderData,
        &rdata,
        renderCb,
    );

    var wl_poll_completion = xev.Completion{
        .op = .{ .poll = .{ .fd = dpy.getFd() } },
        .userdata = dpy,
        .callback = wlPollCb,
    };
    loop.add(&wl_poll_completion);

    if (dpy.dispatchPending() != .SUCCESS) return error.RoundtipFail;
    std.debug.assert(dpy.prepareRead());

    std.log.info("running event loop", .{});
    try loop.run(.until_done);
}

const RenderData = struct {
    gfx: *Gfx,
    egl_dpy: c.EGLDisplay,
    egl_ctx: c.EGLContext,
    outputs: []const OutputWindow,
    output_info: []const OutputInfo,
    last_time: isize,
    base_offset: [2]c_int,
    pointer_state: *PointerState,
    dth: *DrawTimerHandler,
};

fn renderCb(
    data: ?*RenderData,
    loop: *xev.Loop,
    completion: *xev.Completion,
    result: xev.Timer.RunError!void,
) xev.CallbackAction {
    _ = completion;
    result catch unreachable;

    const now = loop.now();
    const delta_time = now - data.?.last_time;
    data.?.last_time = now;

    data.?.dth.resetTimer();

    data.?.gfx.preDraw(
        delta_time,
        data.?.pointer_state,
        data.?.output_info,
        data.?.dth,
    ) catch |e| {
        std.log.err("running preDraw: {}", .{e});
        loop.stop();
        return .disarm;
    };

    for (data.?.outputs, 0..) |output, i| {
        if (!data.?.dth.should_redraw[i])
            continue;

        if (c.eglMakeCurrent(
            data.?.egl_dpy,
            output.egl_surface,
            output.egl_surface,
            data.?.egl_ctx,
        ) != c.EGL_TRUE) {
            std.log.err("failed to set EGL context", .{});
            loop.stop();
            return .disarm;
        }

        data.?.gfx.draw(
            delta_time,
            data.?.pointer_state,
            i,
            data.?.outputs,
            data.?.output_info,
            data.?.dth,
        ) catch |e| {
            std.log.err("drawing: {}", .{e});
            loop.stop();
            return .disarm;
        };
    }

    return data.?.dth.nextAction();
}

fn renderBackgroundCb(
    data: ?*RenderData,
    loop: *xev.Loop,
    completion: *xev.Completion,
    result: xev.Timer.RunError!void,
) xev.CallbackAction {
    result catch unreachable;

    resetXevTimerCompletion(completion, loop.now(), options.refresh_time);

    var rand: f32 = if (options.multihead_mode == .combined) std.crypto.random.float(f32) else 0.0;
    for (data.?.output_info, 0..) |info, i| {
        if (options.multihead_mode == .individual) rand = std.crypto.random.float(f32);
        data.?.gfx.drawBackground(
            info,
            i,
            data.?.base_offset,
            rand,
        ) catch |e| {
            std.log.err("drawing background: {}", .{e});
            loop.stop();
            return .disarm;
        };
    }

    data.?.dth.damageAll();

    return .rearm;
}

fn wlPollCb(
    userdata: ?*anyopaque,
    loop: *xev.Loop,
    _: *xev.Completion,
    result: xev.Result,
) xev.CallbackAction {
    result.poll catch |e| {
        std.log.err("unable to poll wayland FD: {}", .{e});
        loop.stop();
        return .disarm;
    };

    const dpy: *wl.Display = @ptrCast(@alignCast(userdata));
    if (dpy.readEvents() != .SUCCESS) {
        std.log.err("error reading wayland events", .{});
        loop.stop();
        return .disarm;
    }

    while (!dpy.prepareRead()) {
        if (dpy.dispatchPending() != .SUCCESS or dpy.flush() != .SUCCESS) {
            std.log.err("error processing wayland events", .{});
            loop.stop();
            return .disarm;
        }
    }

    return .rearm;
}

fn layerSurfaceListener(lsurf: *zwlr.LayerSurfaceV1, ev: zwlr.LayerSurfaceV1.Event, winsize: *?[2]c_int) void {
    switch (ev) {
        .configure => |configure| {
            winsize.* = .{ @intCast(configure.width), @intCast(configure.height) };
            lsurf.setSize(configure.width, configure.height);
            lsurf.ackConfigure(configure.serial);
        },
        else => {},
    }
}

fn xdgOutputListener(_: *zxdg.OutputV1, ev: zxdg.OutputV1.Event, info: *OutputInfo) void {
    switch (ev) {
        .logical_position => |pos| {
            info.x = pos.x;
            info.y = pos.y;
        },
        .logical_size => |size| {
            info.width = size.width;
            info.height = size.height;
        },
        else => {},
    }
}

const PointerListenerData = struct {
    pstate: *PointerState,
    outputs: []const OutputWindow,
    dth: *DrawTimerHandler,
};

fn pointerListener(_: *wl.Pointer, ev: wl.Pointer.Event, d: *PointerListenerData) void {
    switch (ev) {
        .motion => |motion| {
            if (d.pstate.active_surface_idx) |i| {
                d.pstate.surface_positions[i] = .{
                    motion.surface_x.toInt(),
                    motion.surface_y.toInt(),
                };
                d.dth.damage(i);
            }
        },
        .enter => |enter| {
            for (d.outputs, 0..) |out, i| {
                if (out.surface == enter.surface) {
                    d.dth.damage(i);
                    d.pstate.active_surface_idx = i;
                    break;
                }
            }
        },
        .leave => {
            if (d.pstate.active_surface_idx) |i| {
                d.dth.damage(i);
                d.pstate.active_surface_idx = null;
            }
        },
        else => {},
    }
}

fn resetXevTimerCompletion(completion: *xev.Completion, now: i64, in: i64) void {
    const next_time = now + in;
    completion.op.timer.reset = .{
        .tv_sec = @divTrunc(next_time, std.time.ms_per_s),
        .tv_nsec = @mod(next_time, std.time.ms_per_s) * std.time.ns_per_ms,
    };
}

fn glDebugCb(
    source: c.GLenum,
    @"type": c.GLenum,
    id: c.GLuint,
    severity: c.GLenum,
    len: c.GLsizei,
    msgp: ?[*:0]const u8,
    udata: ?*const anyopaque,
) callconv(.C) void {
    _ = source;
    _ = @"type";
    _ = id;
    _ = udata;
    const log = std.log.scoped(.gl);
    // Mesa likes to include trailing newlines sometimes
    const msg = std.mem.trim(u8, msgp.?[0..@intCast(len)], &std.ascii.whitespace);
    switch (severity) {
        c.GL_DEBUG_SEVERITY_HIGH => log.err("{s}", .{msg}),
        c.GL_DEBUG_SEVERITY_MEDIUM, c.GL_DEBUG_SEVERITY_LOW => log.warn("{s}", .{msg}),
        c.GL_DEBUG_SEVERITY_NOTIFICATION => log.info("{s}", .{msg}),
        else => unreachable,
    }
}
