struct ModelMatrix {
    model: mat4x4<f32>,
    normal_matrix: mat3x3<f32>,
};

struct ObjectData {
    transform_index: u32,
    mesh_id: u32,
    material_id: u32,
    batch_id: u32,
};

struct MeshBounds {
    center: vec3<f32>,
    radius: f32,
};

struct DrawIndexedIndirect {
    index_count: u32,
    instance_count: atomic<u32>,
    first_index: u32,
    base_vertex: i32,
    first_instance: u32,
};

struct CullingUniforms {
    frustum_planes: array<vec4<f32>, 6>,
    object_count: u32,
};

@group(0) @binding(0)
var<storage, read> transforms: array<ModelMatrix>;

@group(0) @binding(1)
var<storage, read> objects: array<ObjectData>;

@group(0) @binding(2)
var<uniform> culling: CullingUniforms;

@group(0) @binding(3)
var<storage, read> mesh_bounds: array<MeshBounds>;

@group(0) @binding(4)
var<storage, read_write> indirect_commands: array<DrawIndexedIndirect>;

@group(0) @binding(5)
var<storage, read_write> visible_indices: array<u32>;

fn sphere_in_frustum(center: vec3<f32>, radius: f32) -> bool {
    for (var i = 0u; i < 6u; i++) {
        let plane = culling.frustum_planes[i];
        let distance = dot(plane.xyz, center) + plane.w;
        if distance < -radius {
            return false;
        }
    }
    return true;
}

@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let object_index = global_id.x;

    if object_index >= culling.object_count {
        return;
    }

    let object = objects[object_index];
    let transform = transforms[object.transform_index];
    let bounds = mesh_bounds[object.mesh_id];

    let local_center = vec4<f32>(bounds.center, 1.0);
    let world_center = transform.model * local_center;
    let world_pos = world_center.xyz;

    let scale_x = length(vec3<f32>(transform.model[0][0], transform.model[0][1], transform.model[0][2]));
    let scale_y = length(vec3<f32>(transform.model[1][0], transform.model[1][1], transform.model[1][2]));
    let scale_z = length(vec3<f32>(transform.model[2][0], transform.model[2][1], transform.model[2][2]));
    let max_scale = max(max(scale_x, scale_y), scale_z);

    let bounding_radius = max_scale * bounds.radius;

    var visible = sphere_in_frustum(world_pos, bounding_radius);

    if visible {
        let batch_id = object.batch_id;
        let write_index = atomicAdd(&indirect_commands[batch_id].instance_count, 1u);
        let first_instance = indirect_commands[batch_id].first_instance;
        visible_indices[first_instance + write_index] = object_index;
    }
}
