const std = @import("std");
const sdl = @import("sdl");
const vk = @import("vulkan");
const builtin = @import("builtin");
const zm = @import("zmath");
const img = @import("zstbi");
const ai = @import("assimp.zig").c;

const QueueUtils = @import("queue_utils.zig");
const StringUtils = @import("string_utils.zig");
const Utilities = @import("utilities.zig");
const Vertex = Utilities.Vertex;
const Vector3 = Utilities.Vector3;

const Context = @import("Context.zig");
const Instance = @import("Context.zig").Instance;
const Swapchain = @import("Swapchain.zig");

const Mesh = @import("Mesh.zig");
const MeshModel = @import("MeshModel.zig");

const MAX_FRAME_DRAWS: u32 = 2;
const MAX_OBJECTS: u32 = 20;
pub const CommandBuffer = vk.CommandBufferProxy(Context.apis);

const UboViewProjection = struct {
    projection: zm.Mat align(16),
    view: zm.Mat align(16),
};

pub const Model = struct {
    model: zm.Mat align(16),
};

pub const VulkanRenderer = struct {
    const Self = @This();

    allocator: std.mem.Allocator,

    current_frame: u32 = 0,

    ctx: Context,
    swapchain: Swapchain,

    // Scene settings
    ubo_view_projection: UboViewProjection,

    // Main
    viewport: vk.Viewport,
    scissor: vk.Rect2D,
    texture_sampler: vk.Sampler,

    depth_buffer_image: []vk.Image,
    depth_buffer_image_memory: []vk.DeviceMemory,
    depth_buffer_image_view: []vk.ImageView,

    colour_buffer_image: []vk.Image,
    colour_buffer_image_memory: []vk.DeviceMemory,
    colour_buffer_image_view: []vk.ImageView,

    // Descriptors
    descriptor_set_layout: vk.DescriptorSetLayout,
    sampler_set_layout: vk.DescriptorSetLayout,
    input_set_layout: vk.DescriptorSetLayout,
    push_constant_range: vk.PushConstantRange,

    descriptor_pool: vk.DescriptorPool,
    sampler_descriptor_pool: vk.DescriptorPool,
    input_descriptor_pool: vk.DescriptorPool,
    descriptor_sets: []vk.DescriptorSet,
    sampler_descriptor_sets: std.ArrayList(vk.DescriptorSet),
    input_descriptor_sets: []vk.DescriptorSet,

    vp_uniform_buffer: []vk.Buffer,
    vp_uniform_buffer_memory: []vk.DeviceMemory,

    // TODO
    command_buffers: []CommandBuffer,

    // Assets
    image_files: std.ArrayList(img.Image),
    texture_images: std.ArrayList(vk.Image),
    texture_image_memory: std.ArrayList(vk.DeviceMemory),
    texture_image_views: std.ArrayList(vk.ImageView),
    model_list: std.ArrayList(MeshModel),

    // Pipeline
    graphics_pipeline: vk.Pipeline,
    pipeline_layout: vk.PipelineLayout,

    second_pipeline: vk.Pipeline,
    second_pipeline_layout: vk.PipelineLayout,

    render_pass: vk.RenderPass,

    // Pools
    graphics_command_pool: vk.CommandPool,

    // Utilities
    depth_format: vk.Format,

    // Synchronisation
    image_available: [MAX_FRAME_DRAWS]vk.Semaphore,
    render_finished: [MAX_FRAME_DRAWS]vk.Semaphore,
    draw_fences: [MAX_FRAME_DRAWS]vk.Fence,

    pub fn init(allocator: std.mem.Allocator, window: sdl.Window) !Self {
        var self: Self = undefined;

        self.allocator = allocator;
        self.ctx = try Context.init(allocator, window);
        self.current_frame = 0;
        self.swapchain = try Swapchain.create(allocator, self.ctx);

        try self.createColourBufferImage();
        try self.createDepthBufferImage();
        try self.createRenderPass();
        try self.createDescriptorSetLayout();
        try self.createPushConstantRange();
        try self.createGraphicsPipeline();
        try self.createFramebuffers();
        try self.createCommandPool();

        self.sampler_descriptor_sets = try std.ArrayList(vk.DescriptorSet).initCapacity(self.allocator, self.swapchain.swapchain_images.len);

        try self.createCommandBuffers();
        try self.createTextureSampler();
        try self.createUniformBuffers();
        try self.createDescriptorPool();
        try self.createDescriptorSets();
        try self.createInputDescriptorSets();
        try self.createSynchronisation();

        self.image_files = std.ArrayList(img.Image).init(self.allocator);
        self.texture_images = std.ArrayList(vk.Image).init(self.allocator);
        self.texture_image_memory = std.ArrayList(vk.DeviceMemory).init(self.allocator);
        self.texture_image_views = std.ArrayList(vk.ImageView).init(self.allocator);
        self.model_list = std.ArrayList(MeshModel).init(allocator);

        const aspect: f32 = @as(f32, @floatFromInt(self.swapchain.extent.width)) / @as(f32, @floatFromInt(self.swapchain.extent.height));
        self.ubo_view_projection.projection = zm.perspectiveFovRh(
            std.math.degreesToRadians(45.0),
            aspect,
            0.1,
            100.0,
        );
        self.ubo_view_projection.view = zm.lookAtRh(
            zm.Vec{ 0.0, 2.0, 2.0, 0.0 },
            zm.Vec{ 0.0, 0.0, 0.0, 0.0 },
            zm.Vec{ 0.0, 1.0, 0.0, 0.0 },
        );

        // Invert y scale
        self.ubo_view_projection.projection[1][1] *= -1;

        _ = try self.createTexture("giraffe.png");

        return self;
    }

    pub fn updateModel(self: *Self, model_id: usize, new_model: zm.Mat) !void {
        if (model_id < self.model_list.items.len) {
            self.model_list.items[model_id].model = new_model;
        }
    }

    pub fn updateCamera(self: *Self, movement: zm.Mat) void {
        self.ubo_view_projection.view = zm.mul(self.ubo_view_projection.view, movement);
    }

    pub fn draw(self: *Self) !void {
        // Wait for given fence to signal (open) from last draw before continuing
        _ = try self.ctx.device.waitForFences(
            1,
            @ptrCast(&self.draw_fences[self.current_frame]),
            vk.TRUE,
            std.math.maxInt(u64),
        );
        // Manually reset (close) fences
        try self.ctx.device.resetFences(1, @ptrCast(&self.draw_fences[self.current_frame]));

        // -- Get next image
        // Get index of next image to be drawn to, and signal semaphore when ready to be drawn to
        const image_index_result = try self.ctx.device.acquireNextImageKHR(
            self.swapchain.handle,
            std.math.maxInt(u64),
            self.image_available[self.current_frame],
            .null_handle,
        );

        try self.recordCommands(image_index_result.image_index);
        try self.updateUniformBuffers(image_index_result.image_index);

        // -- Submit command buffer to render
        // Queue submission information
        const wait_stages = [_]vk.PipelineStageFlags{.{ .color_attachment_output_bit = true }};

        const submit_info: vk.SubmitInfo = .{
            .wait_semaphore_count = 1, // Number of semaphores to wait on
            .p_wait_semaphores = @ptrCast(&self.image_available[self.current_frame]), // List of semaphores to wait on
            .p_wait_dst_stage_mask = &wait_stages, // Stages to check semaphores at
            .command_buffer_count = 1, // Number of command buffers to submit
            .p_command_buffers = @ptrCast(&self.command_buffers[image_index_result.image_index]), // Command buffer to submit
            .signal_semaphore_count = 1, // Number of semaphores to signal
            .p_signal_semaphores = @ptrCast(&self.render_finished[self.current_frame]), // List of semaphores to signal when command buffer finishes
        };

        // Submit command buffer to queue
        try self.ctx.device.queueSubmit(self.ctx.graphics_queue.handle, 1, @ptrCast(&submit_info), self.draw_fences[self.current_frame]);

        // -- Present rendered image to screen
        const present_info: vk.PresentInfoKHR = .{
            .wait_semaphore_count = 1, // Number of semaphores to wait on
            .p_wait_semaphores = @ptrCast(&self.render_finished[self.current_frame]), // Semaphores to wait on
            .swapchain_count = 1, // Number of swapchains to present to
            .p_swapchains = @ptrCast(&self.swapchain.handle), // Swapchains to present images to
            .p_image_indices = @ptrCast(&image_index_result.image_index), // Index of images in swapchains to present
        };

        // Present image
        _ = try self.ctx.device.queuePresentKHR(self.ctx.presentation_queue.handle, &present_info);

        // Get next frame (use % to keep the current frame below MAX_FRAME_DRAWS)
        self.current_frame = (self.current_frame + 1) % MAX_FRAME_DRAWS;
    }

    pub fn deinit(self: *Self) void {
        self.ctx.device.deviceWaitIdle() catch undefined;

        for (0..self.model_list.items.len) |i| {
            self.model_list.items[i].destroy();
        }
        self.model_list.deinit();

        for (0..self.image_files.items.len) |i| {
            self.image_files.items[i].deinit();
        }
        self.image_files.deinit();

        self.ctx.device.destroySampler(self.texture_sampler, null);

        for (
            self.texture_images.items,
            self.texture_image_memory.items,
            self.texture_image_views.items,
        ) |tex_image, tex_image_memory, tex_image_view| {
            self.ctx.device.destroyImage(tex_image, null);
            self.ctx.device.freeMemory(tex_image_memory, null);
            self.ctx.device.destroyImageView(tex_image_view, null);
        }

        self.texture_images.deinit();
        self.texture_image_memory.deinit();
        self.texture_image_views.deinit();

        for (0..self.depth_buffer_image.len) |i| {
            self.ctx.device.destroyImageView(self.depth_buffer_image_view[i], null);
            self.ctx.device.destroyImage(self.depth_buffer_image[i], null);
            self.ctx.device.freeMemory(self.depth_buffer_image_memory[i], null);
        }

        self.allocator.free(self.depth_buffer_image);
        self.allocator.free(self.depth_buffer_image_memory);
        self.allocator.free(self.depth_buffer_image_view);

        for (0..self.colour_buffer_image.len) |i| {
            self.ctx.device.destroyImageView(self.colour_buffer_image_view[i], null);
            self.ctx.device.destroyImage(self.colour_buffer_image[i], null);
            self.ctx.device.freeMemory(self.colour_buffer_image_memory[i], null);
        }

        self.allocator.free(self.colour_buffer_image);
        self.allocator.free(self.colour_buffer_image_memory);
        self.allocator.free(self.colour_buffer_image_view);

        self.ctx.device.destroyDescriptorPool(self.input_descriptor_pool, null);
        self.ctx.device.destroyDescriptorPool(self.descriptor_pool, null);
        self.ctx.device.destroyDescriptorSetLayout(self.descriptor_set_layout, null);
        self.ctx.device.destroyDescriptorPool(self.sampler_descriptor_pool, null);
        self.ctx.device.destroyDescriptorSetLayout(self.sampler_set_layout, null);
        self.ctx.device.destroyDescriptorSetLayout(self.input_set_layout, null);
        self.sampler_descriptor_sets.deinit();
        self.allocator.free(self.input_descriptor_sets);

        for (0..self.swapchain.swapchain_images.len) |i| {
            self.ctx.device.destroyBuffer(self.vp_uniform_buffer[i], null);
            self.ctx.device.freeMemory(self.vp_uniform_buffer_memory[i], null);
        }
        self.allocator.free(self.vp_uniform_buffer);
        self.allocator.free(self.vp_uniform_buffer_memory);
        self.allocator.free(self.descriptor_sets);

        for (0..MAX_FRAME_DRAWS) |i| {
            self.ctx.device.destroySemaphore(self.render_finished[i], null);
            self.ctx.device.destroySemaphore(self.image_available[i], null);
            self.ctx.device.destroyFence(self.draw_fences[i], null);
        }

        self.allocator.free(self.command_buffers);
        self.ctx.device.destroyCommandPool(self.graphics_command_pool, null);

        for (self.swapchain.swapchain_framebuffers) |framebuffer| {
            self.ctx.device.destroyFramebuffer(framebuffer, null);
        }

        self.allocator.free(self.swapchain.swapchain_framebuffers);

        self.ctx.device.destroyPipeline(self.second_pipeline, null);
        self.ctx.device.destroyPipelineLayout(self.second_pipeline_layout, null);
        self.ctx.device.destroyPipeline(self.graphics_pipeline, null);
        self.ctx.device.destroyPipelineLayout(self.pipeline_layout, null);
        self.ctx.device.destroyRenderPass(self.render_pass, null);

        for (self.swapchain.swapchain_images) |swapchain_image| {
            self.ctx.device.destroyImageView(swapchain_image.image_view, null);
        }

        self.allocator.free(self.swapchain.swapchain_images);
        self.ctx.device.destroySwapchainKHR(self.swapchain.handle, null);

        self.ctx.deinit();
    }

    fn createRenderPass(self: *Self) !void {
        // -- Attachments --

        var subpasses: [2]vk.SubpassDescription = undefined;

        // Subpass 1 attachments and references (input attachments)

        // Colour attachment (input)
        const colour_format = chooseSupportedFormat(
            self.ctx.physical_device,
            self.ctx.instance,
            &[_]vk.Format{.r8g8b8a8_srgb},
            .optimal,
            .{ .color_attachment_bit = true },
        );
        const colour_attachment: vk.AttachmentDescription = .{
            .format = colour_format.?,
            .samples = .{ .@"1_bit" = true },
            .load_op = .clear,
            .store_op = .dont_care,
            .stencil_load_op = .dont_care,
            .stencil_store_op = .dont_care,
            .initial_layout = .undefined,
            .final_layout = .color_attachment_optimal,
        };

        // Depth attachment (input)
        const depth_attachment: vk.AttachmentDescription = .{
            .format = self.depth_format,
            .samples = .{ .@"1_bit" = true },
            .load_op = .clear,
            .store_op = .dont_care,
            .stencil_load_op = .dont_care,
            .stencil_store_op = .dont_care,
            .initial_layout = .undefined,
            .final_layout = .depth_stencil_attachment_optimal,
        };

        // Colour attachment (input) reference
        const colour_attachment_reference: vk.AttachmentReference = .{
            .attachment = 1,
            .layout = .color_attachment_optimal,
        };

        // Depth attachment (input) reference
        const depth_attachment_reference: vk.AttachmentReference = .{
            .attachment = 2,
            .layout = .depth_stencil_attachment_optimal,
        };

        subpasses[0] = .{
            .pipeline_bind_point = .graphics, // Pipeline type subpass is to be bound to
            .color_attachment_count = 1,
            .p_color_attachments = @ptrCast(&colour_attachment_reference),
            .p_depth_stencil_attachment = &depth_attachment_reference,
        };

        // Subpass 2 attachments and references

        // Colour attachment of the render pass
        const swapchain_colour_attachment: vk.AttachmentDescription = .{
            .format = self.swapchain.swapchain_image_format, // Format to use for attachment
            .samples = .{ .@"1_bit" = true }, // Number of samples to write for multisampling
            .load_op = .clear, // Describes what to do with attachment before rendering
            .store_op = .store, // Describes what to do with attachment after rendering
            .stencil_load_op = .dont_care, // Describes what to do with stencil before rendering
            .stencil_store_op = .dont_care, // Describes what to do with stencil after rendering
            // Framebuffer data will be stored as an image, but images can be given different data layouts
            // to give optimal use for certain operations
            .initial_layout = vk.ImageLayout.undefined, // Image data layout before render pass starts
            .final_layout = vk.ImageLayout.present_src_khr, // Image data layout after render pass (to change to)
        };

        // Attachment reference uses an attachment index that refers to index in the attachment list passed to render pass create info
        const swapchain_colour_attachment_reference: vk.AttachmentReference = .{
            .attachment = 0,
            .layout = vk.ImageLayout.color_attachment_optimal,
        };

        // References to attachments that subpass will take input from
        const input_references = [_]vk.AttachmentReference{
            .{
                .attachment = 1, // Colour attachment
                .layout = .shader_read_only_optimal,
            },
            .{
                .attachment = 2, // Depth attachment
                .layout = .shader_read_only_optimal,
            },
        };

        subpasses[1] = .{
            .pipeline_bind_point = .graphics,
            .color_attachment_count = 1,
            .p_color_attachments = @ptrCast(&swapchain_colour_attachment_reference),
            .input_attachment_count = @intCast(input_references.len),
            .p_input_attachments = &input_references,
        };

        // -- Subpass dependencies

        // Need to determine when layout transitions occur using subpass dependencies
        const subpass_dependencies = [_]vk.SubpassDependency{
            // Conversion from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
            .{
                // Transition must happen after...
                .src_subpass = vk.SUBPASS_EXTERNAL, // Subpass index (VK_SUBPASS_EXTERNAL = outside of renderpass)
                .src_stage_mask = .{ .bottom_of_pipe_bit = true }, // Pipeline stage
                .src_access_mask = .{ .memory_read_bit = true }, // Stage access mask (memory access)
                // But must happen before...
                .dst_subpass = 0,
                .dst_stage_mask = .{ .color_attachment_output_bit = true },
                .dst_access_mask = .{ .color_attachment_read_bit = true, .color_attachment_write_bit = true },
            },
            // Subpass 1 layout (colour/depth) to subpass 2 layout (shader read)
            .{
                .src_subpass = 0,
                .src_stage_mask = .{ .color_attachment_output_bit = true },
                .src_access_mask = .{ .color_attachment_write_bit = true },
                .dst_subpass = 1,
                .dst_stage_mask = .{ .fragment_shader_bit = true },
                .dst_access_mask = .{ .shader_read_bit = true },
            },
            // Conversion from VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
            .{
                // Transition must happen after...
                .src_subpass = 0,
                .src_stage_mask = .{ .color_attachment_output_bit = true },
                .src_access_mask = .{ .color_attachment_read_bit = true, .color_attachment_write_bit = true },
                // But must happen before...
                .dst_subpass = vk.SUBPASS_EXTERNAL,
                .dst_stage_mask = .{ .bottom_of_pipe_bit = true },
                .dst_access_mask = .{ .memory_read_bit = true },
            },
        };

        // Order matters
        const render_pass_attachments = [_]vk.AttachmentDescription{ swapchain_colour_attachment, colour_attachment, depth_attachment };

        const render_pass_create_info: vk.RenderPassCreateInfo = .{
            .attachment_count = @intCast(render_pass_attachments.len),
            .p_attachments = &render_pass_attachments,
            .subpass_count = @intCast(subpasses.len),
            .p_subpasses = &subpasses,
            .dependency_count = @intCast(subpass_dependencies.len),
            .p_dependencies = &subpass_dependencies,
        };

        self.render_pass = try self.ctx.device.createRenderPass(&render_pass_create_info, null);
    }

    fn createDescriptorSetLayout(self: *Self) !void {
        // -- Uniform values descriptor set layout --

        // UboViewProjection binding info
        const vp_layout_binding: vk.DescriptorSetLayoutBinding = .{
            .binding = 0, // Binding point in shader (designated by binding number in shader)
            .descriptor_type = .uniform_buffer, // Type of descriptor (unifor, dynamic uniform, image sampler, etc)
            .descriptor_count = 1, // Number of descriptors for binding
            .stage_flags = .{ .vertex_bit = true }, // Shader stage to bind to
            .p_immutable_samplers = null, // For texture: can make smapler data immutable by specifying in layout
        };

        const layout_bindings = [_]vk.DescriptorSetLayoutBinding{vp_layout_binding};

        // Create descriptor set layout with given bindings
        const layout_create_info: vk.DescriptorSetLayoutCreateInfo = .{
            .binding_count = @intCast(layout_bindings.len), // Number of binding infos
            .p_bindings = &layout_bindings, // Array of binding infos
        };

        // Create descriptor set layout
        self.descriptor_set_layout = try self.ctx.device.createDescriptorSetLayout(&layout_create_info, null);

        // -- Texture sampler descriptor set layout --

        // Texture binding info
        const sampler_layout_binding: vk.DescriptorSetLayoutBinding = .{
            .binding = 0,
            .descriptor_type = .combined_image_sampler,
            .descriptor_count = 1,
            .stage_flags = .{ .fragment_bit = true },
            .p_immutable_samplers = null,
        };

        // Create a descriptor set layout with given bindings for texture
        const texture_layout_info: vk.DescriptorSetLayoutCreateInfo = .{
            .binding_count = 1,
            .p_bindings = @ptrCast(&sampler_layout_binding),
        };

        self.sampler_set_layout = try self.ctx.device.createDescriptorSetLayout(&texture_layout_info, null);

        // -- Create input attachment image descriptor set layout
        // Colour input binding
        const colour_input_layout_binding: vk.DescriptorSetLayoutBinding = .{
            .binding = 0,
            .descriptor_type = .input_attachment,
            .descriptor_count = 1,
            .stage_flags = .{ .fragment_bit = true },
        };

        // Depth input binding
        const depth_input_layout_binding: vk.DescriptorSetLayoutBinding = .{
            .binding = 1,
            .descriptor_type = .input_attachment,
            .descriptor_count = 1,
            .stage_flags = .{ .fragment_bit = true },
        };

        // Array of input attachment bindings
        const input_bindings = [_]vk.DescriptorSetLayoutBinding{ colour_input_layout_binding, depth_input_layout_binding };

        // Create a descriptor set layout for input attachments
        const input_layout_create_info: vk.DescriptorSetLayoutCreateInfo = .{
            .binding_count = @intCast(input_bindings.len),
            .p_bindings = &input_bindings,
        };

        self.input_set_layout = try self.ctx.device.createDescriptorSetLayout(&input_layout_create_info, null);
    }

    fn createPushConstantRange(self: *Self) !void {
        // Define push constant values (no 'create' needed)
        self.push_constant_range = .{
            .stage_flags = .{ .vertex_bit = true }, // Shader stage push constant will go to
            .offset = 0, // Offset into given data to pass to push constant
            .size = @sizeOf(Model), // Size of data being passed
        };
    }

    fn createColourBufferImage(self: *Self) !void {
        self.colour_buffer_image = try self.allocator.alloc(vk.Image, self.swapchain.swapchain_images.len);
        self.colour_buffer_image_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len);
        self.colour_buffer_image_view = try self.allocator.alloc(vk.ImageView, self.swapchain.swapchain_images.len);

        // Get supported format for colour attachment
        const colour_format = chooseSupportedFormat(
            self.ctx.physical_device,
            self.ctx.instance,
            &[_]vk.Format{.r8g8b8a8_srgb},
            .optimal,
            .{ .color_attachment_bit = true },
        ) orelse return error.FormatNotSupported;

        // Create colour buffers
        for (0..self.colour_buffer_image.len) |i| {
            self.colour_buffer_image[i] = try self.swapchain.createImage(
                self.swapchain.extent.width,
                self.swapchain.extent.height,
                colour_format,
                .optimal,
                .{ .color_attachment_bit = true, .input_attachment_bit = true },
                .{ .device_local_bit = true },
                &self.colour_buffer_image_memory[i],
            );

            self.colour_buffer_image_view[i] = try self.swapchain.createImageView(
                self.colour_buffer_image[i],
                colour_format,
                .{ .color_bit = true },
            );
        }
    }

    fn createDepthBufferImage(self: *Self) !void {
        self.depth_buffer_image = try self.allocator.alloc(vk.Image, self.swapchain.swapchain_images.len);
        self.depth_buffer_image_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len);
        self.depth_buffer_image_view = try self.allocator.alloc(vk.ImageView, self.swapchain.swapchain_images.len);

        // Get supported depth buffer format
        const formats = [_]vk.Format{ .d32_sfloat_s8_uint, .d32_sfloat, .d24_unorm_s8_uint };
        self.depth_format = chooseSupportedFormat(
            self.ctx.physical_device,
            self.ctx.instance,
            &formats,
            .optimal,
            .{ .depth_stencil_attachment_bit = true },
        ) orelse return error.UnsupportedDepthBufferFormat;

        for (0..self.depth_buffer_image.len) |i| {
            // Create depth buffer image
            self.depth_buffer_image[i] = try self.swapchain.createImage(
                self.swapchain.extent.width,
                self.swapchain.extent.height,
                self.depth_format,
                .optimal,
                .{ .depth_stencil_attachment_bit = true, .input_attachment_bit = true },
                .{ .device_local_bit = true },
                &self.depth_buffer_image_memory[i],
            );

            // Create depth buffer image view
            self.depth_buffer_image_view[i] = try self.swapchain.createImageView(self.depth_buffer_image[i], self.depth_format, .{ .depth_bit = true });
        }
    }

    fn createGraphicsPipeline(self: *Self) !void {
        const v_shader align(@alignOf(u32)) = @embedFile("shader_vert").*;
        const f_shader align(@alignOf(u32)) = @embedFile("shader_frag").*;

        // Create shader modules
        const vert = try self.ctx.device.createShaderModule(&.{
            .code_size = v_shader.len,
            .p_code = @ptrCast(&v_shader),
        }, null);
        defer self.ctx.device.destroyShaderModule(vert, null);

        const frag = try self.ctx.device.createShaderModule(&.{
            .code_size = f_shader.len,
            .p_code = @ptrCast(&f_shader),
        }, null);
        defer self.ctx.device.destroyShaderModule(frag, null);

        // -- Shader stage creation information --

        // Vertex stage creation information
        var vertex_shader_create_info: vk.PipelineShaderStageCreateInfo = .{
            .stage = .{ .vertex_bit = true },
            .module = vert,
            .p_name = "main",
        };

        // Fragment stage creation information
        var fragment_shader_create_info: vk.PipelineShaderStageCreateInfo = .{
            .stage = .{ .fragment_bit = true },
            .module = frag,
            .p_name = "main",
        };

        const shader_create_infos = [_]vk.PipelineShaderStageCreateInfo{
            vertex_shader_create_info,
            fragment_shader_create_info,
        };

        // How the data for a single vertex (including info such as position, colour, texture coords, normals, etc...) is as a whole
        const binding_description: vk.VertexInputBindingDescription = .{
            .binding = 0, // Can bind multiple streams of data, this defines which one
            .stride = @sizeOf(Vertex), // Size of simple vertex object
            .input_rate = .vertex, // How to move between data after each vertex
            // vertex: move to the next vertex
            // instance: move to a vertex for the next instance
        };

        // How the data for an attribute is defined within the vertex
        const attribute_descriptions = [_]vk.VertexInputAttributeDescription{
            // Position attribute
            .{
                .binding = 0, // Which binding the data is at (should be same as above)
                .location = 0, // Location in shader where data will be read from
                .format = vk.Format.r32g32b32_sfloat, // Format the data will take (also helps define size of data)
                .offset = @offsetOf(Vertex, "pos"), // Where this attribute is defined in data for a single vertex
            },
            // Colour attribute
            .{
                .binding = 0,
                .location = 1,
                .format = vk.Format.r32g32b32_sfloat,
                .offset = @offsetOf(Vertex, "col"),
            },
            // Texture attribute
            .{
                .binding = 0,
                .location = 2,
                .format = vk.Format.r32g32_sfloat,
                .offset = @offsetOf(Vertex, "tex"),
            },
        };

        // -- Vertex input --
        var vertex_input_create_info: vk.PipelineVertexInputStateCreateInfo = .{
            .vertex_binding_description_count = 1,
            .p_vertex_binding_descriptions = @ptrCast(&binding_description), // List of vertex binding descriptions (data spacing, stride info)
            .vertex_attribute_description_count = @intCast(attribute_descriptions.len),
            .p_vertex_attribute_descriptions = &attribute_descriptions, // List of vertex attribute descriptions (data format and where to bind to/from)
        };

        // -- Input assembly --
        const assembly_create_info: vk.PipelineInputAssemblyStateCreateInfo = .{
            .topology = .triangle_list, // Primitive type to assemble vertices as
            .primitive_restart_enable = vk.FALSE, // Allow overrinding of strip topology to start new primitives
        };

        // -- Viewport & scissor --
        self.viewport = .{
            .x = 0.0,
            .y = 0.0,
            .width = @floatFromInt(self.swapchain.extent.width),
            .height = @floatFromInt(self.swapchain.extent.height),
            .min_depth = 0.0,
            .max_depth = 1.0,
        };

        self.scissor = .{
            .offset = .{ .x = 0, .y = 0 },
            .extent = self.swapchain.extent,
        };

        const viewport_state_create_info: vk.PipelineViewportStateCreateInfo = .{
            .viewport_count = 1,
            .p_viewports = @ptrCast(&self.viewport),
            .scissor_count = 1,
            .p_scissors = @ptrCast(&self.scissor),
        };

        // -- Dynamic states --
        // Dynamic states to enable (TODO: To investigate later)
        const dynamic_states = [_]vk.DynamicState{ .viewport, .scissor };

        const dynamic_state_create_info: vk.PipelineDynamicStateCreateInfo = .{
            .dynamic_state_count = @intCast(dynamic_states.len),
            .p_dynamic_states = &dynamic_states,
        };

        // -- Rasterizer --

        const rasterizer_create_info: vk.PipelineRasterizationStateCreateInfo = .{
            .depth_clamp_enable = vk.FALSE, // Change if fragments beyond near/far planes are clipped (default) or clamped to plane
            .rasterizer_discard_enable = vk.FALSE, // Whether to discard data and skip rasterizer (never creates fragments)
            .polygon_mode = .fill, // How to handle filling points between vertices
            .line_width = 1.0, // How thick the lines should be when drawn
            .cull_mode = .{ .back_bit = false }, // Which face of a triangle to cull
            .front_face = .counter_clockwise, // Winding to determine which side is front
            .depth_bias_enable = vk.FALSE, // Whether to add depth bias to fragments (good for stopping "shadow acne" in shadow mapping)
            .depth_bias_constant_factor = 0,
            .depth_bias_clamp = 0,
            .depth_bias_slope_factor = 0,
        };

        // -- Multisampling --
        const multisampling_create_info: vk.PipelineMultisampleStateCreateInfo = .{
            .sample_shading_enable = vk.FALSE, // Enable multisample shading or not
            .rasterization_samples = .{ .@"1_bit" = true }, // Number of samples to use per fragment
            .min_sample_shading = 1,
            .alpha_to_coverage_enable = vk.FALSE,
            .alpha_to_one_enable = vk.FALSE,
        };

        // -- Blending --

        // Blend attachment state (how blending is handled)
        const colour_state: vk.PipelineColorBlendAttachmentState = .{
            .color_write_mask = .{ .r_bit = true, .g_bit = true, .b_bit = true, .a_bit = true }, // Colours to apply blending to
            .blend_enable = vk.TRUE, // Enable blending
            .src_color_blend_factor = vk.BlendFactor.src_alpha,
            .dst_color_blend_factor = vk.BlendFactor.one_minus_src_alpha,
            .color_blend_op = vk.BlendOp.add,
            // Summary: (VK_BLEND_FACTOR_SRC_ALPHA * new colour) + (VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA * old colour)
            .src_alpha_blend_factor = vk.BlendFactor.one,
            .dst_alpha_blend_factor = vk.BlendFactor.zero,
            .alpha_blend_op = vk.BlendOp.add,
            // Summary (1 * new alpha) + (0 * old alpha) = new alpha
        };
        // Blending uses equation: (srcColorBlendFactor * new colour) colorBlendOp (dstColorBlendFactor * old colour)

        const colour_blending_create_info: vk.PipelineColorBlendStateCreateInfo = .{
            .logic_op_enable = vk.FALSE, // Alternative to calculations is to use logical operations
            .logic_op = .copy,
            .attachment_count = 1,
            .p_attachments = @ptrCast(&colour_state),
            .blend_constants = [_]f32{ 0, 0, 0, 0 },
        };

        // -- Pipeline layout --
        const descriptor_set_layouts = [_]vk.DescriptorSetLayout{ self.descriptor_set_layout, self.sampler_set_layout };

        const pipeline_layout_create_info: vk.PipelineLayoutCreateInfo = .{
            .set_layout_count = @intCast(descriptor_set_layouts.len),
            .p_set_layouts = &descriptor_set_layouts,
            .push_constant_range_count = 1,
            .p_push_constant_ranges = @ptrCast(&self.push_constant_range),
        };

        self.pipeline_layout = try self.ctx.device.createPipelineLayout(&pipeline_layout_create_info, null);

        // -- Depth stencil testing --
        var depth_stencil_create_info: vk.PipelineDepthStencilStateCreateInfo = .{
            .depth_test_enable = vk.TRUE, // Enable checking depth to determine fragment write
            .depth_write_enable = vk.TRUE, // Enable writing to depth buffer to replace all values
            .depth_compare_op = .less, // Comparison operation that allows an overwrite (is in front)
            .depth_bounds_test_enable = vk.FALSE, // Depth bounds test: does the depth value exist between two bounds
            .stencil_test_enable = vk.FALSE, // Enable stencil test
            .front = undefined,
            .back = undefined,
            .min_depth_bounds = undefined,
            .max_depth_bounds = undefined,
        };

        // -- Graphics pipeline creation --
        var pipeline_create_info: vk.GraphicsPipelineCreateInfo = .{
            .stage_count = @intCast(shader_create_infos.len), // Number of shader stages
            .p_stages = &shader_create_infos, // List of shader stages
            .p_vertex_input_state = &vertex_input_create_info,
            .p_input_assembly_state = &assembly_create_info,
            .p_viewport_state = &viewport_state_create_info,
            .p_dynamic_state = &dynamic_state_create_info,
            .p_rasterization_state = &rasterizer_create_info,
            .p_multisample_state = &multisampling_create_info,
            .p_color_blend_state = &colour_blending_create_info,
            .p_depth_stencil_state = &depth_stencil_create_info,
            .layout = self.pipeline_layout, // Pipeline layout the pipeline should use
            .render_pass = self.render_pass, // Renderpass description the pipeline is compatible with
            .subpass = 0, // Subpass of renderpass to use with pipeline
            // Pipeline derivatives: can create multiple pipelines that derive from one another for optimisation
            .base_pipeline_handle = .null_handle, // Existing pipeline to derive from...
            .base_pipeline_index = -1, // Or index of pipeline being created to derive from (in case creating multiple at once)
        };

        _ = try self.ctx.device.createGraphicsPipelines(
            .null_handle,
            1,
            @ptrCast(&pipeline_create_info),
            null,
            @ptrCast(&self.graphics_pipeline),
        );

        // -- Create second pass pipeline
        // Second pass shaders
        const v_second align(@alignOf(u32)) = @embedFile("second_vert").*;
        const f_second align(@alignOf(u32)) = @embedFile("second_frag").*;

        const second_vert_shader_module = try self.ctx.device.createShaderModule(&.{
            .code_size = v_second.len,
            .p_code = @ptrCast(&v_second),
        }, null);
        defer self.ctx.device.destroyShaderModule(second_vert_shader_module, null);

        const second_frag_shader_module = try self.ctx.device.createShaderModule(&.{
            .code_size = f_second.len,
            .p_code = @ptrCast(&f_second),
        }, null);
        defer self.ctx.device.destroyShaderModule(second_frag_shader_module, null);

        // Set new shaders
        vertex_shader_create_info.module = second_vert_shader_module;
        fragment_shader_create_info.module = second_frag_shader_module;

        const second_shader_stages = [_]vk.PipelineShaderStageCreateInfo{ vertex_shader_create_info, fragment_shader_create_info };

        // No vertex data for second pass
        vertex_input_create_info.vertex_binding_description_count = 0;
        vertex_input_create_info.p_vertex_binding_descriptions = null;
        vertex_input_create_info.vertex_attribute_description_count = 0;
        vertex_input_create_info.p_vertex_attribute_descriptions = null;

        // Don't want to write to depth buffer
        depth_stencil_create_info.depth_write_enable = vk.FALSE;

        // Create new pipeline layout
        const second_pipeline_layout_create_info: vk.PipelineLayoutCreateInfo = .{
            .set_layout_count = 1,
            .p_set_layouts = @ptrCast(&self.input_set_layout),
        };

        self.second_pipeline_layout = try self.ctx.device.createPipelineLayout(&second_pipeline_layout_create_info, null);

        pipeline_create_info.stage_count = @intCast(second_shader_stages.len);
        pipeline_create_info.p_stages = &second_shader_stages;
        pipeline_create_info.layout = self.second_pipeline_layout;
        pipeline_create_info.subpass = 1;

        // Create second pipeline
        _ = try self.ctx.device.createGraphicsPipelines(
            .null_handle,
            1,
            @ptrCast(&pipeline_create_info),
            null,
            @ptrCast(&self.second_pipeline),
        );
    }

    fn createFramebuffers(self: *Self) !void {
        self.swapchain.swapchain_framebuffers = try self.allocator.alloc(vk.Framebuffer, self.swapchain.swapchain_images.len);

        // Create a frammebuffer for each swapchain image
        for (self.swapchain.swapchain_images, 0..) |swapchain_image, i| {
            // Order matters
            const attachments = [_]vk.ImageView{
                swapchain_image.image_view,
                self.colour_buffer_image_view[i],
                self.depth_buffer_image_view[i],
            };

            const framebuffer_create_info: vk.FramebufferCreateInfo = .{
                .render_pass = self.render_pass, // Render pass layout the frambuffer will be used with
                .attachment_count = @intCast(attachments.len),
                .p_attachments = &attachments, // List of attachments (1:1 with render pass)
                .width = self.swapchain.extent.width, // Framebuffer width
                .height = self.swapchain.extent.height, // Framebuffer height
                .layers = 1, // Framebuffer layers
            };

            self.swapchain.swapchain_framebuffers[i] = try self.ctx.device.createFramebuffer(&framebuffer_create_info, null);
        }
    }

    fn createCommandPool(self: *Self) !void {
        // Get indices of queue families from device
        const queue_family_indices = try QueueUtils.getQueueFamilies(self.ctx, self.ctx.physical_device);

        const pool_create_info: vk.CommandPoolCreateInfo = .{
            // Queue family type that buffers from this command pool will use
            .queue_family_index = queue_family_indices.graphics_family.?,
            .flags = .{ .reset_command_buffer_bit = true },
        };

        // Create a graphics queue family command pool
        self.graphics_command_pool = try self.ctx.device.createCommandPool(&pool_create_info, null);
    }

    fn createCommandBuffers(self: *Self) !void {
        // Allocate one command buffer for each framebuffer
        const command_buffer_handles = try self.allocator.alloc(vk.CommandBuffer, self.swapchain.swapchain_framebuffers.len);
        defer self.allocator.free(command_buffer_handles);
        self.command_buffers = try self.allocator.alloc(CommandBuffer, command_buffer_handles.len);

        const command_buffer_allocate_info: vk.CommandBufferAllocateInfo = .{
            .command_pool = self.graphics_command_pool,
            .level = .primary, // primary: buffer you submit directly to queue. Can't be called by other buffers
            .command_buffer_count = @intCast(command_buffer_handles.len),
        };

        // Allocate command buffers and place handles in array of buffers
        try self.ctx.device.allocateCommandBuffers(&command_buffer_allocate_info, command_buffer_handles.ptr);
        for (command_buffer_handles, 0..) |command_buffer_handle, i| {
            self.command_buffers[i] = CommandBuffer.init(command_buffer_handle, self.ctx.device.wrapper);
        }
    }

    fn createSynchronisation(self: *Self) !void {
        // Fence create information
        const fence_create_info: vk.FenceCreateInfo = .{ .flags = .{ .signaled_bit = true } };

        // Semaphore creation information
        for (0..MAX_FRAME_DRAWS) |i| {
            self.image_available[i] = try self.ctx.device.createSemaphore(&.{}, null);
            self.render_finished[i] = try self.ctx.device.createSemaphore(&.{}, null);
            self.draw_fences[i] = try self.ctx.device.createFence(&fence_create_info, null);
        }
    }

    fn createTextureSampler(self: *Self) !void {
        // Sampler create info
        const sampler_create_info: vk.SamplerCreateInfo = .{
            .mag_filter = .linear, // How to render when image is magnified on screen
            .min_filter = .linear, // How to render when image is minified on screen
            .address_mode_u = .repeat, // How to handle texture wrap in U (x direction)
            .address_mode_v = .repeat, // How to handle texture wrap in U (y direction)
            .address_mode_w = .repeat, // How to handle texture wrap in U (z direction)
            .border_color = .int_opaque_black, // Border beyond texture (only works for border clamp)
            .unnormalized_coordinates = vk.FALSE, // Whether coords should be normalized (between 0 and 1)
            .mipmap_mode = .linear, // Mipmap interpolation mode
            .mip_lod_bias = 0.0, // Level of detail bias for mip level
            .min_lod = 0.0, // Minimum lod to pick mip level
            .max_lod = 0.0, // Maximum lod to pick mip level
            .anisotropy_enable = vk.TRUE, // Enable anisotropy
            .max_anisotropy = 16.0, // Anisotropy sample level
            .compare_enable = vk.FALSE,
            .compare_op = .never,
        };

        self.texture_sampler = try self.ctx.device.createSampler(&sampler_create_info, null);
    }

    fn createUniformBuffers(self: *Self) !void {
        // View projection buffer size
        const vp_buffer_size: vk.DeviceSize = @sizeOf(UboViewProjection);

        // One uniform buffer for each image (and by extension, command buffer)
        self.vp_uniform_buffer = try self.allocator.alloc(vk.Buffer, self.swapchain.swapchain_images.len);
        self.vp_uniform_buffer_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len);

        // Create the uniform buffers
        for (0..self.vp_uniform_buffer.len) |i| {
            try Utilities.createBuffer(
                self.ctx.physical_device,
                self.ctx.instance,
                self.ctx.device,
                vp_buffer_size,
                .{ .uniform_buffer_bit = true },
                .{ .host_visible_bit = true, .host_coherent_bit = true },
                &self.vp_uniform_buffer[i],
                &self.vp_uniform_buffer_memory[i],
            );
        }
    }

    fn createDescriptorPool(self: *Self) !void {
        // -- Create uniform descriptor pool --

        // Type of descriptors + how many descriptors (!= descriptor sets) (combined makes the pool size)
        // View projection pool
        const vp_pool_size: vk.DescriptorPoolSize = .{
            .type = .uniform_buffer,
            .descriptor_count = @intCast(self.vp_uniform_buffer.len),
        };

        // List of pool sizes
        const descriptor_pool_sizes = [_]vk.DescriptorPoolSize{vp_pool_size};

        // Data to create descriptor pool
        const pool_create_info: vk.DescriptorPoolCreateInfo = .{
            .max_sets = @intCast(self.swapchain.swapchain_images.len), // Maximum number of descriptor sets that can be created from pool
            .pool_size_count = @intCast(descriptor_pool_sizes.len), // Amount of pool sizes being passed
            .p_pool_sizes = &descriptor_pool_sizes, // Pool sizes to create pool with
        };

        // Create descriptor pool
        self.descriptor_pool = try self.ctx.device.createDescriptorPool(&pool_create_info, null);

        // -- Create sampler descriptor pool --

        // Texture sampler pool
        const sampler_pool_size: vk.DescriptorPoolSize = .{
            .type = .combined_image_sampler,
            .descriptor_count = MAX_OBJECTS,
        };

        // FIXME Not the best (look into array layers)
        const sampler_pool_create_info: vk.DescriptorPoolCreateInfo = .{
            .max_sets = MAX_OBJECTS,
            .pool_size_count = 1,
            .p_pool_sizes = @ptrCast(&sampler_pool_size),
        };

        self.sampler_descriptor_pool = try self.ctx.device.createDescriptorPool(&sampler_pool_create_info, null);

        // -- Create input attachment descriptor pool
        // Colour attachment pool size
        const colour_input_pool_size: vk.DescriptorPoolSize = .{
            .type = .input_attachment,
            .descriptor_count = @intCast(self.colour_buffer_image_view.len),
        };

        // Depth attachment pool size
        const depth_input_pool_size: vk.DescriptorPoolSize = .{
            .type = .input_attachment,
            .descriptor_count = @intCast(self.depth_buffer_image_view.len),
        };

        const input_pool_sizes = [_]vk.DescriptorPoolSize{ colour_input_pool_size, depth_input_pool_size };

        // Create input attachment pool
        const input_pool_create_info: vk.DescriptorPoolCreateInfo = .{
            .max_sets = @intCast(self.swapchain.swapchain_images.len),
            .pool_size_count = @intCast(input_pool_sizes.len),
            .p_pool_sizes = &input_pool_sizes,
        };

        self.input_descriptor_pool = try self.ctx.device.createDescriptorPool(&input_pool_create_info, null);
    }

    fn createDescriptorSets(self: *Self) !void {
        // One descriptor set for every buffer
        self.descriptor_sets = try self.allocator.alloc(vk.DescriptorSet, self.swapchain.swapchain_images.len);

        var set_layouts = try self.allocator.alloc(vk.DescriptorSetLayout, self.swapchain.swapchain_images.len);
        defer self.allocator.free(set_layouts);
        for (0..set_layouts.len) |i| {
            set_layouts[i] = self.descriptor_set_layout;
        }

        // Descriptor set allocation info
        const set_alloc_info: vk.DescriptorSetAllocateInfo = .{
            .descriptor_pool = self.descriptor_pool, // Pool to allocate descriptor set from
            .descriptor_set_count = @intCast(self.swapchain.swapchain_images.len), // Number of sets to allocate
            .p_set_layouts = set_layouts.ptr, // Layouts to use to allocate sets (1:1 relationship)
        };

        // Allocate descriptor sets (multiple)
        try self.ctx.device.allocateDescriptorSets(&set_alloc_info, self.descriptor_sets.ptr);

        // Update all of descriptor set buffer bindings
        for (0..self.swapchain.swapchain_images.len) |i| {
            // -- View projection descriptor
            // Buffer info and data offset info
            const vp_buffer_info: vk.DescriptorBufferInfo = .{
                .buffer = self.vp_uniform_buffer[i], // Bufer to get data from
                .offset = 0, // Position of start of data
                .range = @sizeOf(UboViewProjection), // Size of data
            };

            // Data about connection between binding and buffer
            const vp_set_write: vk.WriteDescriptorSet = .{
                .dst_set = self.descriptor_sets[i], // Descriptor set to update
                .dst_binding = 0, // Binding to update (matches with binding on layout/shader)
                .dst_array_element = 0, // Index in array to update
                .descriptor_type = .uniform_buffer, // Type of descriptor
                .descriptor_count = 1, // Amount to update
                .p_buffer_info = @ptrCast(&vp_buffer_info), // Information about buffer data to bind
                .p_image_info = undefined,
                .p_texel_buffer_view = undefined,
            };

            // List of descriptor set writes
            const set_writes = [_]vk.WriteDescriptorSet{vp_set_write};

            // Update the descriptor sets with new buffer/binding info
            self.ctx.device.updateDescriptorSets(@intCast(set_writes.len), &set_writes, 0, null);
        }
    }

    fn createInputDescriptorSets(self: *Self) !void {
        self.input_descriptor_sets = try self.allocator.alloc(vk.DescriptorSet, self.swapchain.swapchain_images.len);

        // Fill array of layouts ready for set creation
        var set_layouts = try self.allocator.alloc(vk.DescriptorSetLayout, self.swapchain.swapchain_images.len);
        defer self.allocator.free(set_layouts);
        for (0..set_layouts.len) |i| {
            set_layouts[i] = self.input_set_layout;
        }

        // Input attachment descriptor set allocation info
        const set_alloc_info: vk.DescriptorSetAllocateInfo = .{
            .descriptor_pool = self.input_descriptor_pool,
            .descriptor_set_count = @intCast(self.swapchain.swapchain_images.len),
            .p_set_layouts = set_layouts.ptr,
        };

        // Allocate descriptor sets
        try self.ctx.device.allocateDescriptorSets(&set_alloc_info, self.input_descriptor_sets.ptr);

        // Update each descriptor set with input attachment
        for (0..self.swapchain.swapchain_images.len) |i| {
            // Colour attachment descriptor
            const colour_attachment_descriptor: vk.DescriptorImageInfo = .{
                .image_layout = .shader_read_only_optimal,
                .image_view = self.colour_buffer_image_view[i],
                .sampler = .null_handle,
            };

            // Colour attachment descriptor write
            const colour_write: vk.WriteDescriptorSet = .{
                .dst_set = self.input_descriptor_sets[i],
                .dst_binding = 0,
                .dst_array_element = 0,
                .descriptor_type = .input_attachment,
                .descriptor_count = 1,
                .p_image_info = @ptrCast(&colour_attachment_descriptor),
                .p_buffer_info = undefined,
                .p_texel_buffer_view = undefined,
            };

            // Depth attachment descriptor
            const depth_attachment_descriptor: vk.DescriptorImageInfo = .{
                .image_layout = .shader_read_only_optimal,
                .image_view = self.depth_buffer_image_view[i],
                .sampler = .null_handle,
            };

            // Depth attachment descriptor write
            const depth_write: vk.WriteDescriptorSet = .{
                .dst_set = self.input_descriptor_sets[i],
                .dst_binding = 1,
                .dst_array_element = 0,
                .descriptor_type = .input_attachment,
                .descriptor_count = 1,
                .p_image_info = @ptrCast(&depth_attachment_descriptor),
                .p_buffer_info = undefined,
                .p_texel_buffer_view = undefined,
            };

            // List of input descriptor set writes
            const set_writes = [_]vk.WriteDescriptorSet{ colour_write, depth_write };

            // Update descriptor sets
            self.ctx.device.updateDescriptorSets(@intCast(set_writes.len), &set_writes, 0, null);
        }
    }

    fn updateUniformBuffers(self: *Self, image_index: u32) !void {
        // Copy VP data
        const data = try self.ctx.device.mapMemory(
            self.vp_uniform_buffer_memory[image_index],
            0,
            @sizeOf(UboViewProjection),
            .{},
        );

        const vp_data: *UboViewProjection = @ptrCast(@alignCast(data));
        vp_data.* = self.ubo_view_projection;
        self.ctx.device.unmapMemory(self.vp_uniform_buffer_memory[image_index]);
    }

    fn recordCommands(self: *Self, current_image: u32) !void {
        // Information about how to begin each command
        const buffer_begin_info: vk.CommandBufferBeginInfo = .{
            // Buffer can be resubmitted when it has already been submitted and is awaiting execution
            .flags = .{ .simultaneous_use_bit = true },
        };

        const clear_values = [_]vk.ClearValue{
            .{ .color = .{ .float_32 = .{ 0.0, 0.0, 0.0, 0.0 } } },
            .{ .color = .{ .float_32 = .{ 0.6, 0.65, 0.4, 1.0 } } },
            .{ .depth_stencil = .{ .depth = 1.0, .stencil = 1 } },
        };

        // Information about how to begin a render pass (only needed for graphical application)
        var render_pass_begin_info: vk.RenderPassBeginInfo = .{
            .render_pass = self.render_pass, // Render pass to begin
            .render_area = .{
                .offset = .{ .x = 0, .y = 0 }, // Start point of render pass in pixels
                .extent = self.swapchain.extent, // Size of region to run render pass on (starting at offset)
            },
            .p_clear_values = &clear_values, // List of clear values
            .clear_value_count = @intCast(clear_values.len),
            .framebuffer = self.swapchain.swapchain_framebuffers[current_image],
        };

        const command_buffer = self.command_buffers[current_image];

        // Start recording commands to command buffer
        try command_buffer.beginCommandBuffer(&buffer_begin_info);

        {
            // Begin render pass
            command_buffer.beginRenderPass(&render_pass_begin_info, vk.SubpassContents.@"inline");

            // Needed when using dynamic state
            command_buffer.setViewport(0, 1, @ptrCast(&self.viewport));
            command_buffer.setScissor(0, 1, @ptrCast(&self.scissor));

            // Bind pipeline to be used in render pass
            command_buffer.bindPipeline(.graphics, self.graphics_pipeline);

            for (self.model_list.items) |model| {
                // Push constants to given shader stage directly (no buffer)
                command_buffer.pushConstants(
                    self.pipeline_layout,
                    .{ .vertex_bit = true }, // Stage to push constants to
                    0, // Offset of push constants to update
                    @sizeOf(Model), // Size of data being pushed
                    @ptrCast(&model.model), // Actual data being pushed (can be array)
                );

                for (model.mesh_list.items) |mesh| {
                    // Buffers to bind
                    const vertex_buffers = [_]vk.Buffer{mesh.vertex_buffer};
                    // Offsets into buffers being bound
                    const offsets = [_]vk.DeviceSize{0};
                    // Command to bind vertex buffer before drawing with them
                    command_buffer.bindVertexBuffers(0, 1, &vertex_buffers, &offsets);

                    // Bind mesh index buffer, with 0 offset and using the uint32 type
                    command_buffer.bindIndexBuffer(mesh.index_buffer, 0, .uint32);

                    const descriptor_set_group = [_]vk.DescriptorSet{
                        self.descriptor_sets[current_image],
                        self.sampler_descriptor_sets.items[mesh.tex_id],
                    };

                    // Bind descriptor sets
                    command_buffer.bindDescriptorSets(
                        .graphics,
                        self.pipeline_layout,
                        0,
                        @intCast(descriptor_set_group.len),
                        &descriptor_set_group,
                        0,
                        null,
                    );

                    // Execute a pipeline
                    command_buffer.drawIndexed(mesh.index_count, 1, 0, 0, 0);
                }
            }

            // Start second subpass
            command_buffer.nextSubpass(.@"inline");

            command_buffer.bindPipeline(.graphics, self.second_pipeline);
            command_buffer.bindDescriptorSets(
                .graphics,
                self.second_pipeline_layout,
                0,
                1,
                @ptrCast(&self.input_descriptor_sets[current_image]),
                0,
                null,
            );
            command_buffer.draw(3, 1, 0, 0);

            // End render pass
            command_buffer.endRenderPass();
        }

        // Stop recording to command buffer
        try command_buffer.endCommandBuffer();
    }

    fn createTextureImage(self: *Self, file_name: []const u8) !u32 {
        // Load image file
        var width: u32 = undefined;
        var height: u32 = undefined;
        var image_size: vk.DeviceSize = undefined;
        const image = try self.loadTextureFile(file_name, &width, &height, &image_size);

        // Create staging buffer to hold loaded data, ready to copy to device
        var image_staging_buffer: vk.Buffer = undefined;
        var image_staging_buffer_memory: vk.DeviceMemory = undefined;
        defer self.ctx.device.destroyBuffer(image_staging_buffer, null);
        defer self.ctx.device.freeMemory(image_staging_buffer_memory, null);

        try Utilities.createBuffer(
            self.ctx.physical_device,
            self.ctx.instance,
            self.ctx.device,
            image_size,
            .{ .transfer_src_bit = true },
            .{ .host_visible_bit = true, .host_coherent_bit = true },
            &image_staging_buffer,
            &image_staging_buffer_memory,
        );

        // Copy data to staging buffer
        const data = try self.ctx.device.mapMemory(image_staging_buffer_memory, 0, image_size, .{});
        const image_data: [*]u8 = @ptrCast(@alignCast(data));

        @memcpy(image_data, image[0..]);
        self.ctx.device.unmapMemory(image_staging_buffer_memory);

        // Create image to hold final texture
        var tex_image_memory: vk.DeviceMemory = undefined;
        const tex_image: vk.Image = try self.swapchain.createImage(
            width,
            height,
            .r8g8b8a8_srgb,
            .optimal,
            .{ .transfer_dst_bit = true, .sampled_bit = true },
            .{ .device_local_bit = true },
            &tex_image_memory,
        );

        // Transition image to be DST for copy operation
        try Utilities.transitionImageLayout(
            self.ctx.device,
            self.ctx.graphics_queue.handle,
            self.graphics_command_pool,
            tex_image,
            .undefined,
            .transfer_dst_optimal,
        );

        // Copy data to image
        try Utilities.copyImageBuffer(
            self.ctx.device,
            self.ctx.graphics_queue.handle,
            self.graphics_command_pool,
            image_staging_buffer,
            tex_image,
            width,
            height,
        );

        // Transition image to be shader readable for shader usage
        try Utilities.transitionImageLayout(
            self.ctx.device,
            self.ctx.graphics_queue.handle,
            self.graphics_command_pool,
            tex_image,
            .transfer_dst_optimal,
            .shader_read_only_optimal,
        );

        // Add texture data to array for reference
        try self.texture_images.append(tex_image);
        try self.texture_image_memory.append(tex_image_memory);

        // Return index of new texture image
        return @intCast(self.texture_images.items.len - 1);
    }

    fn createTexture(self: *Self, file_name: []const u8) !u32 {
        // Create texture image and get its location in the array
        const texture_image_loc = try self.createTextureImage(file_name);

        // Create image view and add to list
        const image_view = try self.swapchain.createImageView(
            self.texture_images.items[texture_image_loc],
            .r8g8b8a8_srgb,
            .{ .color_bit = true },
        );

        try self.texture_image_views.append(image_view);

        // Create texture descriptor
        const descriptor_loc = try self.createTextureDescriptor(image_view);

        // Return location of set with texture
        return descriptor_loc;
    }

    pub fn createMeshModel(self: *Self, model_file: []const u8) !usize {
        const path = try StringUtils.concat("assets/models/", model_file, self.allocator);
        defer self.allocator.free(path);

        // Import model scene
        const scene = ai.aiImportFile(
            path.ptr,
            ai.aiProcess_Triangulate | ai.aiProcess_FlipUVs | ai.aiProcess_JoinIdenticalVertices,
        );
        defer ai.aiReleaseImport(scene);

        // Get array of all materials with 1:1 ID placement
        const texture_names = try MeshModel.loadMaterials(self.allocator, scene);
        defer {
            for (0..texture_names.items.len) |i| {
                if (texture_names.items[i]) |texture_name| {
                    self.allocator.free(texture_name);
                }
            }
            texture_names.deinit();
        }

        // Conversion from the material list IDs to our descriptor array IDs
        var mat_to_tex = try std.ArrayList(u32).initCapacity(self.allocator, texture_names.items.len);
        defer mat_to_tex.deinit();

        // Loop over texture names and create textures for them
        for (texture_names.items) |texture_name| {
            if (texture_name != null) {
                // Create texture and set value to index of new texture
                mat_to_tex.appendAssumeCapacity(try self.createTexture(texture_name.?));
            } else {

                // If material had no texture, set to 0 to indicate no texture. Texture 0 will be reserver for a default texture
                mat_to_tex.appendAssumeCapacity(0);
            }
        }

        // Load in all our meshes
        const model_meshes = try MeshModel.loadNode(
            self.allocator,
            self.ctx.instance,
            self.ctx.physical_device,
            self.ctx.device,
            self.ctx.graphics_queue.handle,
            self.graphics_command_pool,
            scene.*.mRootNode,
            scene,
            mat_to_tex.items,
        );

        // Create a mesh model and add to our list
        const mesh_model = MeshModel.new(self.allocator, model_meshes);
        try self.model_list.append(mesh_model);

        return self.model_list.items.len - 1;
    }

    fn createTextureDescriptor(self: *Self, texture_image: vk.ImageView) !u32 {
        var descriptor_set: vk.DescriptorSet = undefined;

        // Descriptor set allocation info
        const set_alloc_info: vk.DescriptorSetAllocateInfo = .{
            .descriptor_pool = self.sampler_descriptor_pool,
            .descriptor_set_count = 1,
            .p_set_layouts = @ptrCast(&self.sampler_set_layout),
        };

        // Allocate descriptor sets
        try self.ctx.device.allocateDescriptorSets(&set_alloc_info, @ptrCast(&descriptor_set));

        const image_info: vk.DescriptorImageInfo = .{
            .image_layout = .shader_read_only_optimal, // Image layout when in use
            .image_view = texture_image, // Image to bind to set
            .sampler = self.texture_sampler, // Sampler to use for set
        };

        // Descriptor write info
        const descriptor_write: vk.WriteDescriptorSet = .{
            .dst_set = descriptor_set,
            .dst_binding = 0,
            .dst_array_element = 0,
            .descriptor_type = .combined_image_sampler,
            .descriptor_count = 1,
            .p_image_info = @ptrCast(&image_info),
            .p_buffer_info = undefined,
            .p_texel_buffer_view = undefined,
        };

        // Update the new descriptor set
        self.ctx.device.updateDescriptorSets(1, @ptrCast(&descriptor_write), 0, null);

        try self.sampler_descriptor_sets.append(descriptor_set);

        // Return descriptor set location
        return @intCast(self.sampler_descriptor_sets.items.len - 1);
    }

    fn loadTextureFile(self: *Self, file_name: []const u8, width: *u32, height: *u32, image_size: *vk.DeviceSize) ![]const u8 {
        const path_concat = [2][]const u8{ "./assets/textures/", file_name };
        const path = try std.mem.concatWithSentinel(self.allocator, u8, &path_concat, 0);
        defer self.allocator.free(path);

        const image = try img.Image.loadFromFile(path, 0);
        try self.image_files.append(image);

        width.* = image.width;
        height.* = image.height;

        // Calculate image size using given and known data
        image_size.* = width.* * height.* * 4;

        return image.data;
    }
};

fn chooseSupportedFormat(
    pdev: vk.PhysicalDevice,
    instance: Instance,
    formats: []const vk.Format,
    tiling: vk.ImageTiling,
    feature_flags: vk.FormatFeatureFlags,
) ?vk.Format {
    // Loop through the options and find a compatible one

    // Depending on tiling choice. Need to check for different bit flag
    for (formats) |format| {
        // Get properties for given format on this device
        const properties = instance.getPhysicalDeviceFormatProperties(pdev, format);

        if (tiling == .linear and properties.linear_tiling_features.contains(feature_flags)) {
            return format;
        } else if (tiling == .optimal and properties.optimal_tiling_features.contains(feature_flags)) {
            return format;
        }
    }

    return null;
}
