const std = @import("std");
const Allocator = std.mem.Allocator;

const zgl = @import("zgl");
const Shape = @import("zmesh").Shape;

const helper = @import("root.zig");
const Camera = @import("camera.zig");
const Shader = @import("shader.zig");
const Texture = @import("texture.zig");

index_count: usize,
primitive_type: zgl.PrimitiveType,
textures: ?[]const Texture,

position_data_buffer: zgl.Buffer,
uv_data_buffer: ?zgl.Buffer,
normal_data_buffer: ?zgl.Buffer,

vao: zgl.VertexArray,

pub fn initShape(shape: Shape, textures: ?[]const Texture, primitive_type: zgl.PrimitiveType) @This() {
    return initVertices(
        shape.positions,
        shape.texcoords,
        shape.normals,
        shape.indices,
        textures,
        primitive_type,
    );
}

pub fn initVertices(
    positions: []const [3]f32,
    maybe_texcoords: ?[]const [2]f32,
    maybe_normals: ?[]const [3]f32,
    indices: []const Shape.IndexType,
    textures: ?[]const Texture,
    primitive_type: zgl.PrimitiveType,
) @This() {
    const vao = zgl.VertexArray.create();

    const position_data_buffer = helper.initSsbo([3]f32, positions, .{});
    var uv_data_buffer: ?zgl.Buffer = null;
    var normal_data_buffer: ?zgl.Buffer = null;

    if (maybe_texcoords) |texcoords| {
        uv_data_buffer = helper.initSsbo([2]f32, texcoords, .{});
    }

    if (maybe_normals) |normals| {
        normal_data_buffer = helper.initSsbo([3]f32, normals, .{});
    }

    const ebo = helper.initEbo(Shape.IndexType, indices);
    defer ebo.delete();
    vao.elementBuffer(ebo);

    return .{
        .index_count = indices.len,
        .textures = textures,
        .primitive_type = primitive_type,
        .vao = vao,
        .position_data_buffer = position_data_buffer,
        .uv_data_buffer = uv_data_buffer,
        .normal_data_buffer = normal_data_buffer,
    };
}

pub fn draw(self: @This(), shader: Shader) void {
    drawInstanced(self, shader, 1);
}

pub fn drawInstanced(self: @This(), shader: Shader, instance_count: u32) void {
    shader.use();
    self.vao.bind();

    if (self.textures) |textures| {
        textures[0].bind();
        shader.updateUniformInt("u_texture", @intCast(textures[0].unit));
    }

    zgl.bindBufferBase(.shader_storage_buffer, 0, self.position_data_buffer);

    if (self.uv_data_buffer) |uv_buffer| {
        zgl.bindBufferBase(.shader_storage_buffer, 1, uv_buffer);
    }

    if (self.normal_data_buffer) |normal_buffer| {
        zgl.bindBufferBase(.shader_storage_buffer, 2, normal_buffer);
    }

    if (Shape.IndexType == u32) {
        zgl.drawElementsInstanced(self.primitive_type, self.index_count, .unsigned_int, 0, instance_count);
    } else {
        zgl.drawElementsInstanced(self.primitive_type, self.index_count, .unsigned_short, 0, instance_count);
    }

    if (self.textures) |textures| {
        for (textures) |tex| {
            zgl.bindTextureUnit(.invalid, tex.unit);
        }
    }

    zgl.bindBufferBase(.shader_storage_buffer, 0, .invalid);
    zgl.bindBufferBase(.shader_storage_buffer, 1, .invalid);
    zgl.bindBufferBase(.shader_storage_buffer, 2, .invalid);
}

pub fn delete(self: @This()) void {
    self.vao.delete();
    if (self.textures) |textures| {
        for (textures) |tex| {
            tex.delete();
        }
    }
    self.position_data_buffer.delete();
    if (self.uv_data_buffer) |uv_buffer| {
        uv_buffer.delete();
    }

    if (self.normal_data_buffer) |normal_buffer| {
        normal_buffer.delete();
    }
}
