const std = @import("std");
const builtin = @import("builtin");
const vk = @import("vk");
const nu = @import("../../nu.zig");

const Debug = @import("Debug.zig");

const config = nu.config.render;

pub const versions: []const vk.ApiInfo = &.{
    vk.features.version_1_0,
    vk.features.version_1_1,
    vk.features.version_1_2,
    vk.features.version_1_3,
};

pub const instance_exts: []const vk.ApiInfo = if (config.use_debug_messenger) &.{
    vk.extensions.ext_debug_utils,
    vk.extensions.khr_surface,
} else &.{
    vk.extensions.khr_surface,
};

pub const device_exts: []const vk.ApiInfo = &.{
    vk.extensions.khr_swapchain,
    vk.extensions.khr_dynamic_rendering,
    vk.extensions.khr_timeline_semaphore,
};

pub const apis = versions ++ instance_exts ++ device_exts;

pub const layers: []const [*:0]const u8 =
    if (config.use_debug_messenger) &.{
    "VK_LAYER_KHRONOS_validation",
} else &.{};

pub const BaseWrapper = vk.BaseWrapper(apis);
pub const InstanceWrapper = vk.InstanceWrapper(apis);
pub const DeviceWrapper = vk.DeviceWrapper(apis);
pub const InstanceProxy = vk.InstanceProxy(apis);
pub const DeviceProxy = vk.DeviceProxy(apis);
pub const QueueProxy = vk.QueueProxy(apis);
pub const CommandBufferProxy = vk.CommandBufferProxy(apis);

var _iw: InstanceWrapper = undefined;
pub const iw: *const InstanceWrapper = &_iw;
var _dw: DeviceWrapper = undefined;
pub const dw: *const DeviceWrapper = &_dw;

var _B: BaseWrapper = undefined;
pub const B: *const BaseWrapper = &_B;
var _I: InstanceProxy = undefined;
pub const I: *const InstanceProxy = &_I;
var _D: DeviceProxy = undefined;
pub const D: *const DeviceProxy = &_D;
var _Q: QueueProxy = undefined;
pub const Q: *const QueueProxy = &_Q;

var _instance: vk.Instance = undefined;
pub const instance: *const vk.Instance = &_instance;
var _messenger: if (config.use_debug_messenger) vk.DebugUtilsMessengerEXT else void = undefined;
pub const messenger: *const if (config.use_debug_messenger) vk.DebugUtilsMessengerEXT else void = &_messenger;
var _surface: vk.SurfaceKHR = undefined;
pub const surface: *const vk.SurfaceKHR = &_surface;
var _pdevice: vk.PhysicalDevice = undefined;
pub const pdevice: *const vk.PhysicalDevice = &_pdevice;
var _device: vk.Device = undefined;
pub const device: *const vk.Device = &_device;
var _family: u32 = undefined;
pub const family: *const u32 = &_family;
var _queue: vk.Queue = undefined;
pub const queue: *const vk.Queue = &_queue;

pub fn init(alloc: std.mem.Allocator) !void {
    _B = if (config.use_debug_messenger)
        try BaseWrapper.load(glfwGetInstanceProcAddress)
    else
        BaseWrapper.loadNoFail(glfwGetInstanceProcAddress);

    _instance = try _create_instance(alloc);
    _iw = if (config.use_debug_messenger)
        try InstanceWrapper.load(_instance, glfwGetInstanceProcAddress)
    else
        InstanceWrapper.loadNoFail(_instance, glfwGetInstanceProcAddress);
    errdefer _destroy_instance();
    _I = InstanceProxy.init(_instance, iw);

    if (config.use_debug_messenger) _messenger = try _create_messenger();
    errdefer if (config.use_debug_messenger) _destroy_messenger();

    _surface = try _create_surface();
    errdefer _destroy_surface();

    _pdevice = try _select_pdevice(alloc);
    _family = try _select_queue_family_index(alloc); // only one queue supported
    _device = try _create_device(alloc);
    _dw = if (config.use_debug_messenger)
        try DeviceWrapper.load(_device, iw.dispatch.vkGetDeviceProcAddr)
    else
        DeviceWrapper.loadNoFail(_device, iw.dispatch.vkGetDeviceProcAddr);
    errdefer _destroy_device();
    _D = DeviceProxy.init(_device, dw);
    _queue = D.getDeviceQueue(_family, 0); // only one queue supported
    _Q = QueueProxy.init(_queue, dw);
}

pub fn deinit() void {
    _destroy_device();
    _destroy_surface();
    if (config.use_debug_messenger) _destroy_messenger();
    _destroy_instance();
}

fn _create_instance(alloc: std.mem.Allocator) !vk.Instance {
    var extnames = std.ArrayList([*:0]const u8).init(alloc);
    defer extnames.deinit();

    for (instance_exts) |ext|
        try extnames.append(ext.name);

    var glfw_exts_count: u32 = 0;
    const glfw_exts: [*]const [*:0]const u8 =
        glfwGetRequiredInstanceExtensions(&glfw_exts_count);
    try extnames.appendSlice(glfw_exts[0..glfw_exts_count]);

    var ci: vk.InstanceCreateInfo = .{
        .p_application_info = &vk.ApplicationInfo{
            .p_application_name = config.app_name,
            .application_version = vk.makeApiVersion(
                config.app_version.variant,
                config.app_version.major,
                config.app_version.minor,
                config.app_version.patch,
            ),
            .p_engine_name = config.engine_name,
            .engine_version = vk.makeApiVersion(
                config.engine_version.variant,
                config.engine_version.major,
                config.engine_version.minor,
                config.engine_version.patch,
            ),
            .api_version = vk.features.version_1_3.version,
        },
        .enabled_extension_count = @intCast(extnames.items.len),
        .pp_enabled_extension_names = extnames.items.ptr,
        .enabled_layer_count = @intCast(layers.len),
        .pp_enabled_layer_names = layers.ptr,
    };

    if (config.use_debug_messenger) ci.p_next = &Debug.ci;

    return try B.createInstance(&ci, null);
}

fn _destroy_instance() void {
    I.destroyInstance(null);
}

fn _create_messenger() !vk.DebugUtilsMessengerEXT {
    return try I.createDebugUtilsMessengerEXT(&Debug.ci, null);
}

fn _destroy_messenger() void {
    I.destroyDebugUtilsMessengerEXT(_messenger, null);
}

fn _create_surface() !vk.SurfaceKHR {
    var res: vk.SurfaceKHR = undefined;
    if (glfwCreateWindowSurface(
        _instance,
        nu.Window.handle,
        null,
        &res,
    ) != .success) {
        return error.CreateWindowSurfaceFailed;
    }
    return res;
}

fn _destroy_surface() void {
    I.destroySurfaceKHR(_surface, null);
}

fn _select_pdevice(alloc: std.mem.Allocator) !vk.PhysicalDevice {
    var count: u32 = undefined;
    _ = try I.enumeratePhysicalDevices(
        &count,
        null,
    );
    const pdevs = try alloc.alloc(vk.PhysicalDevice, count);
    defer alloc.free(pdevs);
    _ = try I.enumeratePhysicalDevices(
        &count,
        pdevs.ptr,
    );

    const scores = try alloc.alloc(i32, count);
    @memset(scores, 0);
    defer alloc.free(scores);

    for (pdevs, scores) |pdev, *score| {
        const props = I.getPhysicalDeviceProperties(pdev);
        score.* += switch (props.device_type) {
            .discrete_gpu => 1000,
            .integrated_gpu => 500,
            else => 0,
        };
    }

    const idx = std.mem.indexOfMax(i32, scores);

    return pdevs[idx];
}

fn _select_queue_family_index(alloc: std.mem.Allocator) !u32 {
    var count: u32 = undefined;
    I.getPhysicalDeviceQueueFamilyProperties(
        _pdevice,
        &count,
        null,
    );
    const families = try alloc.alloc(vk.QueueFamilyProperties, count);
    defer alloc.free(families);
    I.getPhysicalDeviceQueueFamilyProperties(
        _pdevice,
        &count,
        families.ptr,
    );

    for (families, 0..) |prop, idx| {
        if (!prop.queue_flags.graphics_bit) continue;

        if (!prop.queue_flags.transfer_bit) continue;

        if (try I.getPhysicalDeviceSurfaceSupportKHR(
            _pdevice,
            @intCast(idx),
            _surface,
        ) != vk.TRUE) continue;

        return @intCast(idx);
    }

    return error.NoSuitableQueueFamily;
}

fn _create_device(alloc: std.mem.Allocator) !vk.Device {
    const qci: []const vk.DeviceQueueCreateInfo = &.{
        vk.DeviceQueueCreateInfo{
            .queue_count = 1,
            .queue_family_index = @intCast(_family),
            .p_queue_priorities = &[_]f32{1.0},
        },
    };

    var extnames = std.ArrayList([*:0]const u8).init(alloc);
    defer extnames.deinit();

    for (device_exts) |ext|
        try extnames.append(ext.name);

    const ci: vk.DeviceCreateInfo = .{
        .queue_create_info_count = @intCast(qci.len),
        .p_queue_create_infos = qci.ptr,
        .enabled_extension_count = @intCast(extnames.items.len),
        .pp_enabled_extension_names = extnames.items.ptr,
        .p_next = &vk.PhysicalDeviceDynamicRenderingFeaturesKHR{
            .dynamic_rendering = vk.TRUE,
        },
    };

    return try I.createDevice(_pdevice, &ci, null);
}

fn _destroy_device() void {
    D.deviceWaitIdle() catch |err| switch (err) {
        error.OutOfHostMemory,
        error.OutOfDeviceMemory,
        error.DeviceLost,
        => {
            // In these cases we would destroy the device anyway, so just fall through. Note any child objects must
            // already be destroyed. This assumes normal cleanup has been done before _destroy_device was called.
        },
        else => unreachable,
    };
    D.destroyDevice(null);
}

pub fn getPhysicalDeviceSurfaceCapabilities() !vk.SurfaceCapabilitiesKHR {
    return try I.getPhysicalDeviceSurfaceCapabilitiesKHR(_pdevice, _surface);
}

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

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

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

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