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

const vk = @import("vk");
const glfw = @cImport({
    @cDefine("GLFW_INCLUDE_NONE", {});
    @cInclude("GLFW/glfw3.h");
});

const apis: []const vk.ApiInfo = &(.{
    vk.features.version_1_0,
    vk.features.version_1_1,
    vk.features.version_1_2,
    vk.features.version_1_3,
    vk.extensions.khr_surface,
    vk.extensions.khr_swapchain,
    vk.extensions.khr_dynamic_rendering,
    vk.extensions.khr_timeline_semaphore,
} ++ switch (builtin.mode) {
    .Debug, .ReleaseSafe => .{
        vk.extensions.ext_debug_utils,
    },
    else => .{},
});

pub const Base = vk.BaseWrapper(apis);
pub const Instance = vk.InstanceProxy(apis);
pub const Device = vk.DeviceProxy(apis);
pub const Queue = vk.QueueProxy(apis);

pub const SwapChain = @import("swapchain.zig").SwapChain;

const Core = @This();

alloc: std.mem.Allocator,
b: *Base,
i: Instance,
d: Device,
q: Queue,
window: *glfw.GLFWwindow,
pdev: vk.PhysicalDevice,
surface: vk.SurfaceKHR,
msg: switch (builtin.mode) {
    .Debug, .ReleaseSafe => vk.DebugUtilsMessengerEXT,
    else => void,
},

pub fn init(
    alloc: std.mem.Allocator,
    options: struct {
        width: u32 = 1280,
        height: u32 = 720,
        title: []const u8 = "Hello World",
    },
) !Core {
    var self: Core = undefined;
    self.alloc = alloc;

    var arena = std.heap.ArenaAllocator.init(alloc);
    defer arena.deinit();
    const ialloc = arena.allocator();

    const titlez = try ialloc.dupeZ(u8, options.title);

    if (glfw.glfwInit() != glfw.GLFW_TRUE) {
        return error.GLFWInitFailed;
    }
    errdefer glfw.glfwTerminate();

    glfw.glfwWindowHint(glfw.GLFW_CLIENT_API, glfw.GLFW_NO_API);
    glfw.glfwWindowHintString(glfw.GLFW_X11_CLASS_NAME, "floating_window");
    glfw.glfwWindowHintString(glfw.GLFW_X11_INSTANCE_NAME, "floating_window");
    self.window = glfw.glfwCreateWindow(
        @intCast(options.width),
        @intCast(options.height),
        titlez,
        null,
        null,
    ) orelse
        return error.GLFWWindowFailed;

    const bw = try alloc.create(vk.BaseWrapper(apis));
    errdefer alloc.destroy(bw);
    bw.* = try vk.BaseWrapper(apis).load(glfwGetInstanceProcAddress);
    self.b = bw;

    var iexts = std.ArrayList([*:0]const u8).init(ialloc);
    var ilyrs = std.ArrayList([*:0]const u8).init(ialloc);

    switch (builtin.mode) {
        .Debug, .ReleaseSafe => {
            try iexts.appendSlice(&.{
                vk.extensions.ext_debug_utils.name,
            });
            try ilyrs.appendSlice(&.{
                "VK_LAYER_KHRONOS_validation",
            });
        },
        else => {},
    }

    {
        var count: u32 = undefined;
        const ptr = glfwGetRequiredInstanceExtensions(&count);
        try iexts.appendSlice(ptr[0..count]);
    }

    var dmci = switch (builtin.mode) {
        .Debug, .ReleaseSafe => vk.DebugUtilsMessengerCreateInfoEXT{
            .message_severity = .{
                .error_bit_ext = true,
                .info_bit_ext = true,
                .verbose_bit_ext = true,
                .warning_bit_ext = true,
            },
            .message_type = .{
                .device_address_binding_bit_ext = true,
                .general_bit_ext = true,
                .performance_bit_ext = true,
                .validation_bit_ext = true,
            },
            .pfn_user_callback = &debug_callback,
            .p_user_data = null,
        },
        else => {},
    };

    var ici: vk.InstanceCreateInfo = .{
        .p_application_info = &vk.ApplicationInfo{
            .api_version = vk.API_VERSION_1_3,
            .engine_version = 0,
            .application_version = 0,
            .p_application_name = titlez,
            .p_engine_name = titlez,
        },
        .enabled_extension_count = @intCast(iexts.items.len),
        .pp_enabled_extension_names = iexts.items.ptr,
        .enabled_layer_count = @intCast(ilyrs.items.len),
        .pp_enabled_layer_names = ilyrs.items.ptr,
    };

    switch (builtin.mode) {
        .Debug, .ReleaseSafe => {
            ici.p_next = &dmci;
        },
        else => {},
    }

    self.i.handle = try self.b.createInstance(&ici, null);
    const iw = try alloc.create(vk.InstanceWrapper(apis));
    errdefer alloc.destroy(iw);
    iw.* = try vk.InstanceWrapper(apis).load(self.i.handle, bw.dispatch.vkGetInstanceProcAddr);
    self.i.wrapper = iw;
    errdefer self.i.destroyInstance(null);

    self.msg = switch (builtin.mode) {
        .Debug, .ReleaseSafe => try self.i.createDebugUtilsMessengerEXT(&dmci, null),
        else => {},
    };
    errdefer switch (builtin.mode) {
        .Debug, .ReleaseSafe => self.i.destroyDebugUtilsMessengerEXT(self.msg, null),
        else => {},
    };

    // Device selection. Just use the first one.
    const pdevs: []vk.PhysicalDevice = try self.i.enumeratePhysicalDevicesAlloc(ialloc);
    std.debug.assert(pdevs.len >= 1);
    self.pdev = pdevs[0];

    switch (glfwCreateWindowSurface(
        self.i.handle,
        self.window,
        null,
        &self.surface,
    )) {
        .success => {},
        else => return error.GLFWWindowSurfaceFailed,
    }
    errdefer self.i.destroySurfaceKHR(self.surface, null);

    // Queue selection. Just use the first one.
    const qci: []const vk.DeviceQueueCreateInfo = &.{
        vk.DeviceQueueCreateInfo{
            .queue_family_index = 0,
            .queue_count = 1,
            .p_queue_priorities = &.{1},
        },
    };

    var dexts = std.ArrayList([*:0]const u8).init(ialloc);
    var dlyrs = std.ArrayList([*:0]const u8).init(ialloc);

    _ = &dlyrs;

    try dexts.appendSlice(&.{
        vk.extensions.khr_swapchain.name,
        vk.extensions.khr_dynamic_rendering.name,
        vk.extensions.khr_timeline_semaphore.name,
    });

    self.d.handle = try self.i.createDevice(self.pdev, &.{
        .enabled_extension_count = @intCast(dexts.items.len),
        .pp_enabled_extension_names = dexts.items.ptr,
        .enabled_layer_count = @intCast(dlyrs.items.len),
        .pp_enabled_layer_names = dlyrs.items.ptr,
        .queue_create_info_count = @intCast(qci.len),
        .p_queue_create_infos = qci.ptr,
        .p_next = &vk.PhysicalDeviceDynamicRenderingFeatures{
            .dynamic_rendering = vk.TRUE,
        },
    }, null);
    const dw = try alloc.create(vk.DeviceWrapper(apis));
    dw.* = try vk.DeviceWrapper(apis).load(self.d.handle, iw.dispatch.vkGetDeviceProcAddr);
    self.d.wrapper = dw;
    errdefer self.d.destroyDevice(null);

    self.q.handle = self.d.getDeviceQueue(0, 0);
    self.q.wrapper = dw;

    return self;
}

pub fn deinit(self: Core) void {
    self.d.destroyDevice(null);

    self.i.destroySurfaceKHR(self.surface, null);

    switch (builtin.mode) {
        .Debug, .ReleaseSafe => self.i.destroyDebugUtilsMessengerEXT(self.msg, null),
        else => {},
    }

    self.i.destroyInstance(null);

    self.alloc.destroy(self.d.wrapper);
    self.alloc.destroy(self.i.wrapper);
    self.alloc.destroy(self.b);

    glfw.glfwTerminate();
}

extern fn glfwGetInstanceProcAddress(
    instance: vk.Instance,
    procname: [*:0]const u8,
) vk.PfnVoidFunction;

extern fn glfwGetRequiredInstanceExtensions(
    count: *u32,
) [*][*:0]const u8;

fn debug_callback(
    msg_severity: vk.DebugUtilsMessageSeverityFlagsEXT,
    msg_type: vk.DebugUtilsMessageTypeFlagsEXT,
    p_data: ?*const vk.DebugUtilsMessengerCallbackDataEXT,
    _: ?*anyopaque,
) callconv(vk.vulkan_call_conv) vk.Bool32 {
    // ripped from std.log.defaultLog

    const data = p_data orelse return vk.FALSE;
    const message = data.p_message orelse return vk.FALSE;

    const severity_prefix = if (msg_severity.verbose_bit_ext)
        "verbose:"
    else if (msg_severity.info_bit_ext)
        "info:"
    else if (msg_severity.warning_bit_ext)
        "warning:"
    else if (msg_severity.error_bit_ext)
        "error:"
    else
        "?:";

    const type_prefix = if (msg_type.general_bit_ext)
        ""
    else if (msg_type.validation_bit_ext)
        "validation:"
    else if (msg_type.performance_bit_ext)
        "performance:"
    else if (msg_type.device_address_binding_bit_ext)
        "device_address_binding:"
    else
        "?:";

    const stderr = std.io.getStdErr().writer();
    var bw = std.io.bufferedWriter(stderr);
    const writer = bw.writer();

    std.debug.lockStdErr();
    defer std.debug.unlockStdErr();
    nosuspend {
        writer.print("vk-{s}{s} {s}\n", .{ severity_prefix, type_prefix, message }) catch return vk.FALSE;
        bw.flush() catch return vk.FALSE;
    }

    return vk.FALSE;
}

extern fn glfwCreateWindowSurface(
    instance: vk.Instance,
    window: *glfw.GLFWwindow,
    allocation_callbacks: ?*const vk.AllocationCallbacks,
    surface: *vk.SurfaceKHR,
) vk.Result;

extern fn glfwGetPhysicalDevicePresentationSupport(
    instance: vk.Instance,
    pdev: vk.PhysicalDevice,
    queuefamily: u32,
) c_int;
