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

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

fn _choose_format(core: *const Core, alloc: std.mem.Allocator) !vk.SurfaceFormatKHR {
    const formats = try core.i.getPhysicalDeviceSurfaceFormatsAllocKHR(
        core.pdev,
        core.surface,
        alloc,
    );
    defer alloc.free(formats);

    for (formats) |format| {
        if (format.color_space == .srgb_nonlinear_khr) return format;
    } else {
        return formats[0];
    }
}

fn _choose_mode(core: *const Core, alloc: std.mem.Allocator) !vk.PresentModeKHR {
    _ = core;
    _ = alloc;

    return .fifo_khr;
}

const Self = @This();

pub const Target = struct {
    image_index: u32,
    flight_index: u32,
    image: vk.Image,
    view: vk.ImageView,
    acquired: vk.Semaphore, // this semaphore will be signaled when the target is acquired
    complete: vk.Semaphore, // this semaphore should be signaled when the render is complete
    available: vk.Fence, // this fence should be signaled when the target flight is available

    pub fn top_of_pipe_barrier(target: Target) vk.ImageMemoryBarrier {
        return vk.ImageMemoryBarrier{
            .src_access_mask = .{},
            .dst_access_mask = .{ .color_attachment_write_bit = true },
            .old_layout = .undefined,
            .new_layout = .color_attachment_optimal,
            .src_queue_family_index = 0,
            .dst_queue_family_index = 0,
            .image = target.image,
            .subresource_range = .{
                .aspect_mask = .{ .color_bit = true },
                .base_mip_level = 0,
                .level_count = 1,
                .base_array_layer = 0,
                .layer_count = 1,
            },
        };
    }

    pub fn bottom_of_pipe_barrier(target: Target) vk.ImageMemoryBarrier {
        return vk.ImageMemoryBarrier{
            .src_access_mask = .{ .color_attachment_write_bit = true },
            .dst_access_mask = .{},
            .old_layout = .color_attachment_optimal,
            .new_layout = .present_src_khr,
            .src_queue_family_index = 0,
            .dst_queue_family_index = 0,
            .image = target.image,
            .subresource_range = .{
                .aspect_mask = .{ .color_bit = true },
                .base_mip_level = 0,
                .level_count = 1,
                .base_array_layer = 0,
                .layer_count = 1,
            },
        };
    }
};

const Sync = struct {
    acquired: vk.Semaphore = .null_handle,
    complete: vk.Semaphore = .null_handle,
    available: vk.Fence = .null_handle,
};

const View = struct {
    image: vk.Image,
    view: vk.ImageView,
};

core: *const Core,
alloc: std.mem.Allocator,

frames_in_flight: u32,
flight_index: usize = 0,
flight_syncs: std.MultiArrayList(Sync) = .{},

cinfo: vk.SwapchainCreateInfoKHR = undefined,
handle: vk.SwapchainKHR = .null_handle,

chain: std.MultiArrayList(View) = .{},

pub fn init(core: *const Core, alloc: std.mem.Allocator, frames_in_flight: usize) !Self {
    var self: Self = .{
        .core = core,
        .alloc = alloc,
        .frames_in_flight = @intCast(frames_in_flight),
    };
    errdefer self.deinit();

    try self.flight_syncs.resize(alloc, frames_in_flight);
    for (self.flight_syncs.items(.acquired)) |*sem|
        sem.* = try core.d.createSemaphore(&.{}, null);
    for (self.flight_syncs.items(.complete)) |*sem|
        sem.* = try core.d.createSemaphore(&.{}, null);
    for (self.flight_syncs.items(.available)) |*fnc|
        fnc.* = try core.d.createFence(&.{ .flags = .{ .signaled_bit = true } }, null);

    const caps = try core.i.getPhysicalDeviceSurfaceCapabilitiesKHR(core.pdev, core.surface);
    const format = try _choose_format(core, alloc);
    const mode = try _choose_mode(core, alloc);

    self.cinfo = .{
        .surface = core.surface,
        .min_image_count = std.math.clamp(
            @min(3, caps.min_image_count + 1),
            caps.min_image_count,
            if (caps.max_image_count > 0) caps.max_image_count else 127,
        ),
        .image_format = format.format,
        .image_color_space = format.color_space,
        .image_extent = undefined, // set in rebuild
        .image_array_layers = 1,
        .image_usage = .{ .color_attachment_bit = true },
        .image_sharing_mode = .exclusive,
        .pre_transform = .{ .identity_bit_khr = true },
        .composite_alpha = .{ .opaque_bit_khr = true },
        .present_mode = mode,
        .clipped = vk.TRUE,
        .old_swapchain = .null_handle,
    };

    // try self.rebuild();

    return self;
}

pub fn rebuild(self: *Self) !void {
    const caps = try self.core.i.getPhysicalDeviceSurfaceCapabilitiesKHR(self.core.pdev, self.core.surface);
    self.cinfo.image_extent = caps.current_extent;

    self.handle = try self.core.d.createSwapchainKHR(&self.cinfo, null);
    self.core.d.destroySwapchainKHR(self.cinfo.old_swapchain, null);
    self.cinfo.old_swapchain = self.handle;

    for (self.chain.items(.view)) |view| self.core.d.destroyImageView(view, null);
    @memset(self.chain.items(.view), .null_handle);
    @memset(self.chain.items(.image), .null_handle);

    var count: u32 = undefined;
    std.debug.assert(
        .success == try self.core.d.getSwapchainImagesKHR(
            self.handle,
            &count,
            null,
        ),
    );
    try self.chain.resize(self.alloc, count);
    std.debug.assert(
        .success == try self.core.d.getSwapchainImagesKHR(
            self.handle,
            &count,
            self.chain.items(.image).ptr,
        ),
    );

    for (self.chain.items(.image), self.chain.items(.view)) |image, *view| {
        view.* = try self.core.d.createImageView(&vk.ImageViewCreateInfo{
            .image = image,
            .view_type = .@"2d",
            .format = self.cinfo.image_format,
            .components = .{
                .r = .identity,
                .g = .identity,
                .b = .identity,
                .a = .identity,
            },
            .subresource_range = .{
                .aspect_mask = .{ .color_bit = true },
                .base_mip_level = 0,
                .level_count = 1,
                .base_array_layer = 0,
                .layer_count = 1,
            },
        }, null);
    }
}

pub fn deinit(self: *Self) void {
    // The easiest way to ensure fences and semaphores are not in use for deletion. If it fails, the device must
    // have failed somehow and sync is not necessary so just continue with cleanup.
    self.core.d.deviceWaitIdle() catch {};

    // images are owned by swapchain and not explicitly destroyed
    for (self.chain.items(.view)) |view| self.core.d.destroyImageView(view, null);
    self.chain.deinit(self.alloc);

    self.core.d.destroySwapchainKHR(self.handle, null);

    for (self.flight_syncs.items(.acquired)) |sem| self.core.d.destroySemaphore(sem, null);
    for (self.flight_syncs.items(.complete)) |sem| self.core.d.destroySemaphore(sem, null);
    for (self.flight_syncs.items(.available)) |fnc| self.core.d.destroyFence(fnc, null);
    self.flight_syncs.deinit(self.alloc);
}

pub fn acquire(self: *Self) !?Target {
    var target: Target = .{
        .flight_index = @intCast(self.flight_index),
        .acquired = self.flight_syncs.items(.acquired)[self.flight_index],
        .complete = self.flight_syncs.items(.complete)[self.flight_index],
        .available = self.flight_syncs.items(.available)[self.flight_index],
        .image = undefined,
        .view = undefined,
        .image_index = undefined,
    };

    const timeout = std.math.maxInt(u64);

    std.debug.assert(.success == try self.core.d.waitForFences(
        1,
        &.{target.available},
        vk.TRUE,
        std.math.maxInt(u64),
    ));

    // two attempts

    target.image_index = for (0..2) |_| {
        if (self.handle == .null_handle) try self.rebuild();

        if (self.core.d.acquireNextImageKHR(
            self.handle,
            timeout,
            target.acquired,
            .null_handle,
        )) |res| switch (res.result) {
            .success, .suboptimal_khr => break res.image_index,
            else => unreachable,
        } else |err| switch (err) {
            error.OutOfDateKHR => {
                self.handle = .null_handle;
            },
            else => return err,
        }
    } else {
        return null;
    };

    target.image = self.chain.items(.image)[target.image_index];
    target.view = self.chain.items(.view)[target.image_index];

    try self.core.d.resetFences(1, &.{target.available});
    self.flight_index = @mod(self.flight_index + 1, self.flight_syncs.len);

    return target;
}

pub fn present(self: *Self, target: Target) !void {
    if (self.core.q.presentKHR(&vk.PresentInfoKHR{
        .wait_semaphore_count = 1,
        .p_wait_semaphores = &.{target.complete},
        .swapchain_count = 1,
        .p_swapchains = &.{self.handle},
        .p_image_indices = &.{target.image_index},
        .p_results = null,
    })) |res| switch (res) {
        .success => {},
        .suboptimal_khr => self.handle = .null_handle,
        else => unreachable,
    } else |err| switch (err) {
        error.OutOfDateKHR => self.handle = .null_handle,
        else => return err,
    }
}
