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

const Mesh = @import("Mesh.zig");
const Device = @import("Context.zig").Device;
const Instance = @import("Context.zig").Instance;
const Vertex = @import("utilities.zig").Vertex;

const Self = @This();

allocator: std.mem.Allocator,

mesh_list: std.ArrayList(Mesh),
model: zm.Mat,

pub fn new(allocator: std.mem.Allocator, mesh_list: std.ArrayList(Mesh)) Self {
    var new_mesh_model: Self = undefined;

    new_mesh_model.allocator = allocator;
    new_mesh_model.mesh_list = mesh_list;
    new_mesh_model.model = zm.identity();

    return new_mesh_model;
}

pub fn destroy(self: *Self) void {
    for (0..self.mesh_list.items.len) |i| {
        self.mesh_list.items[i].destroyBuffers();
    }
    self.mesh_list.deinit();
}

pub fn getMesh(self: Self, idx: usize) !Mesh {
    if (idx >= self.mesh_list.len - 1) {
        return error.MeshIndexOutOfBounds;
    }

    return self.mesh_list.items[idx];
}

pub fn loadMaterials(allocator: std.mem.Allocator, scene: *const ai.aiScene) !std.ArrayList(?[]const u8) {
    // Create 1:1 sized list of textures
    var texture_list = try std.ArrayList(?[]const u8).initCapacity(allocator, scene.mNumMaterials);

    // Go through each material and copy its texture file name (if it exists)
    for (0..scene.mNumMaterials) |i| {
        // Get the material
        const material = scene.mMaterials[i];

        // Initialise the texture to empty string (will be replaced if the texture exists)
        // try texture_list.append("");

        // Check for diffuse texture (standard detail texture)
        if (ai.aiGetMaterialTextureCount(material, ai.aiTextureType_DIFFUSE) != 0) {
            // Get the path of the texture file
            var path: ai.aiString = undefined;
            if (ai.aiGetMaterialTexture(
                material,
                ai.aiTextureType_DIFFUSE,
                0,
                &path,
                null,
                null,
                null,
                null,
                null,
                null,
            ) == ai.AI_SUCCESS) {
                // Cut of any directory information already present
                var it = std.mem.splitBackwardsAny(u8, &path.data, "\\/");
                if (it.next()) |filename| {
                    texture_list.appendAssumeCapacity(try allocator.dupe(u8, filename));
                }
            } else {
                texture_list.appendAssumeCapacity(null);
            }
        } else {
            texture_list.appendAssumeCapacity(null);
        }
    }

    return texture_list;
}

pub fn loadNode(
    allocator: std.mem.Allocator,
    instance: Instance,
    pdev: vk.PhysicalDevice,
    device: Device,
    transfer_queue: vk.Queue,
    transfer_command_pool: vk.CommandPool,
    node: *const ai.aiNode,
    scene: *const ai.aiScene,
    mat_to_tex: []u32,
) !std.ArrayList(Mesh) {
    var mesh_list = std.ArrayList(Mesh).init(allocator);

    // Go through each mesh at this node and create it, then add it to our mesh list
    for (0..node.mNumMeshes) |i| {
        // Load mesh here
        try mesh_list.append(try loadMesh(
            allocator,
            instance,
            pdev,
            device,
            transfer_queue,
            transfer_command_pool,
            scene.mMeshes[node.mMeshes[i]],
            mat_to_tex,
        ));
    }

    // Go through each node attached to this node and load it, then append their meshes to this node's mesh list
    for (0..node.mNumChildren) |i| {
        var new_list = try loadNode(
            allocator,
            instance,
            pdev,
            device,
            transfer_queue,
            transfer_command_pool,
            node.mChildren[i],
            scene,
            mat_to_tex,
        );
        defer new_list.deinit();

        try mesh_list.appendSlice(new_list.items[0..]);
    }

    return mesh_list;
}

pub fn loadMesh(
    allocator: std.mem.Allocator,
    instance: Instance,
    pdev: vk.PhysicalDevice,
    device: Device,
    transfer_queue: vk.Queue,
    transfer_command_pool: vk.CommandPool,
    mesh: *const ai.aiMesh,
    mat_to_tex: []u32,
) !Mesh {
    var vertices = try std.ArrayList(Vertex).initCapacity(allocator, mesh.mNumVertices);
    var indices = std.ArrayList(u32).init(allocator);
    defer vertices.deinit();
    defer indices.deinit();

    // Go through each vertex and copy it across to our vertices
    for (0..mesh.mNumVertices) |i| {
        var vertex: Vertex = undefined;

        // Set position
        vertex.pos = .{ mesh.mVertices[i].x, mesh.mVertices[i].y, mesh.mVertices[i].z };

        // Set texture coords (if they exist)
        if (mesh.mTextureCoords[0] != null) {
            vertex.tex = .{ mesh.mTextureCoords[0][i].x, mesh.mTextureCoords[0][i].y };
        } else {
            vertex.tex = .{ 0.0, 0.0 };
        }

        // Set colour (just use white for now)
        vertex.col = .{ 1.0, 1.0, 1.0 };

        // vertices.items[i] = vertex;
        vertices.appendAssumeCapacity(vertex);
    }

    // Iterate over indices through faces and copy across
    for (0..mesh.mNumFaces) |i| {
        // Get a face
        const face = mesh.mFaces[i];

        // Go through face's indices and add to list
        for (0..face.mNumIndices) |j| {
            try indices.append(face.mIndices[j]);
        }
    }

    return try Mesh.new(
        instance,
        pdev,
        device,
        transfer_queue,
        transfer_command_pool,
        vertices.items,
        indices.items,
        mat_to_tex[mesh.mMaterialIndex],
        allocator,
    );
}
