const std = @import("std");
const vk = @import("vulkan");
const zm = @import("zmath");

const Utilities = @import("utilities.zig");
const Vertex = Utilities.Vertex;
const Device = @import("Context.zig").Device;
const Instance = @import("Context.zig").Instance;
const Model = @import("vulkan_renderer.zig").Model;

const Self = @This();

ubo_model: Model,
tex_id: u32,

vertex_count: u32,
vertex_buffer: vk.Buffer,
vertex_buffer_memory: vk.DeviceMemory,

index_count: u32,
index_buffer: vk.Buffer,
index_buffer_memory: vk.DeviceMemory,

instance: Instance,
physical_device: vk.PhysicalDevice,
device: Device,

allocator: std.mem.Allocator,

pub fn new(
    instance: Instance,
    pdev: vk.PhysicalDevice,
    device: Device,
    transfer_queue: vk.Queue,
    transfer_command_pool: vk.CommandPool,
    vertices: []const Vertex,
    indices: []const u32,
    tex_id: u32,
    allocator: std.mem.Allocator,
) !Self {
    var self: Self = undefined;

    self.vertex_count = @intCast(vertices.len);
    self.index_count = @intCast(indices.len);

    self.instance = instance;
    self.physical_device = pdev;
    self.device = device;
    self.allocator = allocator;

    try self.createVertexBuffer(transfer_queue, transfer_command_pool, vertices);
    try self.createIndexBuffer(transfer_queue, transfer_command_pool, indices);

    self.ubo_model = .{ .model = zm.identity() };
    self.tex_id = tex_id;

    return self;
}

pub fn destroyBuffers(self: Self) void {
    self.device.destroyBuffer(self.vertex_buffer, null);
    self.device.freeMemory(self.vertex_buffer_memory, null);

    self.device.destroyBuffer(self.index_buffer, null);
    self.device.freeMemory(self.index_buffer_memory, null);
}

fn createVertexBuffer(
    self: *Self,
    transfer_queue: vk.Queue,
    transfer_command_pool: vk.CommandPool,
    vertices: []const Vertex,
) !void {
    // Get size of buffer needed for vertices
    const buffer_size: vk.DeviceSize = @sizeOf(Vertex) * vertices.len;

    // Temporary buffer to "stage" vertex data before transfering to GPU
    var staging_buffer: vk.Buffer = undefined;
    var staging_buffer_memory: vk.DeviceMemory = undefined;
    defer self.device.destroyBuffer(staging_buffer, null);
    defer self.device.freeMemory(staging_buffer_memory, null);

    // Create buffer and allocate memory to it
    try Utilities.createBuffer(
        self.physical_device,
        self.instance,
        self.device,
        buffer_size,
        .{ .transfer_src_bit = true },
        .{ .host_visible_bit = true, .host_coherent_bit = true },
        &staging_buffer,
        &staging_buffer_memory,
    );

    // Map memory to vertex
    // 1. Create pointer to a point in normal memory
    // 2. Map the vertex buffer memory to that point
    const data = try self.device.mapMemory(staging_buffer_memory, 0, buffer_size, .{});
    // 3. Copy memory from vertices vector to the point in memory
    const gpu_vertices: [*]Vertex = @ptrCast(@alignCast(data));
    @memcpy(gpu_vertices, vertices[0..]);
    // 4. Unmap the vertex buffer memory
    self.device.unmapMemory(staging_buffer_memory);

    // ---

    // Create buffer with TRANSFER_DST_BIT to mark as recipient of transfer data (also VERTEX_BUFFER)
    // Buffer memory is to be DEVICE_LOCAL_BIT meaning memory is on the GPU and only accessible by it and not CPU (host)
    try Utilities.createBuffer(
        self.physical_device,
        self.instance,
        self.device,
        buffer_size,
        .{ .transfer_dst_bit = true, .vertex_buffer_bit = true },
        .{ .device_local_bit = true },
        &self.vertex_buffer,
        &self.vertex_buffer_memory,
    );

    // Copy staging buffer to vertex buffer on GPU
    try Utilities.copyBuffer(
        self.device,
        transfer_queue,
        transfer_command_pool,
        staging_buffer,
        self.vertex_buffer,
        buffer_size,
    );
}

fn createIndexBuffer(
    self: *Self,
    transfer_queue: vk.Queue,
    transfer_command_pool: vk.CommandPool,
    indices: []const u32,
) !void {
    // Get size of buffer needed for indices
    const buffer_size: vk.DeviceSize = @sizeOf(u32) * indices.len;

    // Temporary buffer to "stage" vertex data before transfering to GPU
    var staging_buffer: vk.Buffer = undefined;
    var staging_buffer_memory: vk.DeviceMemory = undefined;
    defer self.device.destroyBuffer(staging_buffer, null);
    defer self.device.freeMemory(staging_buffer_memory, null);

    try Utilities.createBuffer(
        self.physical_device,
        self.instance,
        self.device,
        buffer_size,
        .{ .transfer_src_bit = true },
        .{ .host_visible_bit = true, .host_coherent_bit = true },
        &staging_buffer,
        &staging_buffer_memory,
    );

    // Map memory to index buffer
    const data = try self.device.mapMemory(staging_buffer_memory, 0, buffer_size, .{});
    const gpu_vertices: [*]u32 = @ptrCast(@alignCast(data));
    @memcpy(gpu_vertices, indices[0..]);
    self.device.unmapMemory(staging_buffer_memory);

    // Create buffer for index data on GPU access only
    try Utilities.createBuffer(
        self.physical_device,
        self.instance,
        self.device,
        buffer_size,
        .{ .transfer_dst_bit = true, .index_buffer_bit = true },
        .{ .device_local_bit = true },
        &self.index_buffer,
        &self.index_buffer_memory,
    );

    // Copy from staging buffer to GPU access buffer
    try Utilities.copyBuffer(
        self.device,
        transfer_queue,
        transfer_command_pool,
        staging_buffer,
        self.index_buffer,
        buffer_size,
    );
}
