const builtin = @import("builtin");
const std = @import("std");

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

// NOTE: to prevent the exe from having multiple markers, I can't create a separate string literal
//       for the marker and get the length from that, I have to hardcode the length
const exe_marker_len = 42;

// I'm exporting this and making it mutable to make sure the compiler keeps it around
// and prevent it from evaluting its contents at comptime
export var zig_exe_string: [exe_marker_len + std.fs.max_path_bytes + 1]u8 =
    ("!!!THIS MARKS THE zig_exe_string MEMORY!!#" ++ ([1]u8{0} ** (std.fs.max_path_bytes + 1))).*;

const global = struct {
    var child: std.process.Child = undefined;
    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const arena = arena_instance.allocator();
};

pub fn main() !u8 {
    // Sanity check that the exe_marker_len is right (note: not fullproof)
    std.debug.assert(zig_exe_string[exe_marker_len - 1] == '#');
    if (zig_exe_string[exe_marker_len] == 0) {
        log.err("the zig target executable has not been set in the exelink", .{});
        return 0xff; // fail
    }
    var zig_exe_len: usize = 1;
    while (zig_exe_string[exe_marker_len + zig_exe_len] != 0) {
        zig_exe_len += 1;
        if (exe_marker_len + zig_exe_len > std.fs.max_path_bytes) {
            log.err("the zig target execuable is either too big (over {}) or the exe is corrupt", .{std.fs.max_path_bytes});
            return 1;
        }
    }
    const zig_exe = zig_exe_string[exe_marker_len .. exe_marker_len + zig_exe_len :0];

    const args = try std.process.argsAlloc(global.arena);
    if (args.len >= 2 and std.mem.eql(u8, args[1], "exelink")) {
        try std.io.getStdOut().writer().writeAll(zig_exe);
        return 0;
    }
    args[0] = zig_exe;

    // NOTE: create the process.child before calling SetConsoleCtrlHandler because it uses it
    global.child = std.process.Child.init(args, global.arena);

    if (0 == win32.SetConsoleCtrlHandler(consoleCtrlHandler, 1)) {
        log.err("SetConsoleCtrlHandler failed, error={}", .{@intFromEnum(win32.GetLastError())});
        return 0xff; // fail
    }

    try global.child.spawn();
    return switch (try global.child.wait()) {
        .Exited => |e| e,
        .Signal => 0xff,
        .Stopped => 0xff,
        .Unknown => 0xff,
    };
}

fn consoleCtrlHandler(ctrl_type: u32) callconv(@import("std").os.windows.WINAPI) win32.BOOL {
    //
    // NOTE: Do I need to synchronize this with the main thread?
    //
    const name: []const u8 = switch (ctrl_type) {
        win32.CTRL_C_EVENT => "Control-C",
        win32.CTRL_BREAK_EVENT => "Break",
        win32.CTRL_CLOSE_EVENT => "Close",
        win32.CTRL_LOGOFF_EVENT => "Logoff",
        win32.CTRL_SHUTDOWN_EVENT => "Shutdown",
        else => "Unknown",
    };
    // TODO: should we stop the process on a break event?
    log.info("caught ctrl signal {d} ({s}), stopping process...", .{ ctrl_type, name });
    const exit_code = switch (global.child.kill() catch |err| {
        log.err("failed to kill process, error={s}", .{@errorName(err)});
        std.process.exit(0xff);
    }) {
        .Exited => |e| e,
        .Signal => 0xff,
        .Stopped => 0xff,
        .Unknown => 0xff,
    };
    std.process.exit(exit_code);
    unreachable;
}

const win32 = struct {
    pub const BOOL = i32;
    pub const CTRL_C_EVENT = @as(u32, 0);
    pub const CTRL_BREAK_EVENT = @as(u32, 1);
    pub const CTRL_CLOSE_EVENT = @as(u32, 2);
    pub const CTRL_LOGOFF_EVENT = @as(u32, 5);
    pub const CTRL_SHUTDOWN_EVENT = @as(u32, 6);
    pub const GetLastError = std.os.windows.kernel32.GetLastError;
    pub const PHANDLER_ROUTINE = switch (builtin.zig_backend) {
        .stage1 => fn (
            CtrlType: u32,
        ) callconv(@import("std").os.windows.WINAPI) BOOL,
        else => *const fn (
            CtrlType: u32,
        ) callconv(@import("std").os.windows.WINAPI) BOOL,
    };
    pub extern "kernel32" fn SetConsoleCtrlHandler(
        HandlerRoutine: ?PHANDLER_ROUTINE,
        Add: BOOL,
    ) callconv(@import("std").os.windows.WINAPI) BOOL;
};