pub(super) fn reverse_z_ortho_light(
    left: f32,
    right: f32,
    bottom: f32,
    top: f32,
    near: f32,
    far: f32,
) -> nalgebra_glm::Mat4 {
    let width = right - left;
    let height = top - bottom;
    let depth = far - near;

    nalgebra_glm::Mat4::new(
        2.0 / width,
        0.0,
        0.0,
        -(right + left) / width,
        0.0,
        2.0 / height,
        0.0,
        -(top + bottom) / height,
        0.0,
        0.0,
        1.0 / depth,
        -near / depth,
        0.0,
        0.0,
        0.0,
        1.0,
    )
}

pub(super) fn get_frustum_corners_world_space(
    view: &nalgebra_glm::Mat4,
    fov: f32,
    aspect: f32,
    near: f32,
    far: f32,
) -> [nalgebra_glm::Vec3; 8] {
    let inv_view = nalgebra_glm::inverse(view);
    let tan_half_fov = (fov / 2.0).tan();

    let near_height = near * tan_half_fov;
    let near_width = near_height * aspect;
    let far_height = far * tan_half_fov;
    let far_width = far_height * aspect;

    let corners_view = [
        nalgebra_glm::vec3(-near_width, -near_height, -near),
        nalgebra_glm::vec3(near_width, -near_height, -near),
        nalgebra_glm::vec3(near_width, near_height, -near),
        nalgebra_glm::vec3(-near_width, near_height, -near),
        nalgebra_glm::vec3(-far_width, -far_height, -far),
        nalgebra_glm::vec3(far_width, -far_height, -far),
        nalgebra_glm::vec3(far_width, far_height, -far),
        nalgebra_glm::vec3(-far_width, far_height, -far),
    ];

    let mut corners_world = [nalgebra_glm::vec3(0.0, 0.0, 0.0); 8];
    for (index, corner) in corners_view.iter().enumerate() {
        let world_pos = inv_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
        corners_world[index] = nalgebra_glm::vec3(world_pos.x, world_pos.y, world_pos.z);
    }
    corners_world
}

pub struct CascadeViewProjectionResult {
    pub view_projection: nalgebra_glm::Mat4,
    pub cascade_diameter: f32,
}

pub(super) fn calculate_cascade_view_projection(
    frustum_corners: &[nalgebra_glm::Vec3; 8],
    light_direction: &nalgebra_glm::Vec3,
    _cascade_resolution: f32,
    _cascade_far: f32,
) -> CascadeViewProjectionResult {
    let mut center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
    for corner in frustum_corners {
        center += corner;
    }
    center /= 8.0;

    let mut max_radius = 0.0f32;
    for corner in frustum_corners {
        let dist = nalgebra_glm::length(&(corner - center));
        max_radius = max_radius.max(dist);
    }

    let up = if light_direction.y.abs() > 0.99 {
        nalgebra_glm::vec3(1.0, 0.0, 0.0)
    } else {
        nalgebra_glm::vec3(0.0, 1.0, 0.0)
    };

    let light_dir_normalized = light_direction.normalize();

    let light_view = nalgebra_glm::look_at(
        &(center - light_dir_normalized * max_radius * 4.0),
        &center,
        &up,
    );

    let mut min_x = f32::MAX;
    let mut max_x = f32::MIN;
    let mut min_y = f32::MAX;
    let mut max_y = f32::MIN;
    let mut min_z = f32::MAX;
    let mut max_z = f32::MIN;

    for corner in frustum_corners {
        let light_space = light_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
        min_x = min_x.min(light_space.x);
        max_x = max_x.max(light_space.x);
        min_y = min_y.min(light_space.y);
        max_y = max_y.max(light_space.y);
        min_z = min_z.min(light_space.z);
        max_z = max_z.max(light_space.z);
    }

    let padding = (max_x - min_x).max(max_y - min_y) * 0.1;
    min_x -= padding;
    max_x += padding;
    min_y -= padding;
    max_y += padding;

    let z_mult = 10.0;
    if min_z < 0.0 {
        min_z *= z_mult;
    } else {
        min_z /= z_mult;
    }
    if max_z < 0.0 {
        max_z /= z_mult;
    } else {
        max_z *= z_mult;
    }

    let light_projection = reverse_z_ortho_light(min_x, max_x, min_y, max_y, min_z, max_z);

    let cascade_diameter = max_radius * 2.0;

    CascadeViewProjectionResult {
        view_projection: light_projection * light_view,
        cascade_diameter,
    }
}

pub fn extract_frustum_planes(view_proj: &crate::ecs::world::Mat4) -> [crate::ecs::world::Vec4; 6] {
    let mut planes = [crate::ecs::world::Vec4::zeros(); 6];

    let row0 = crate::ecs::world::Vec4::new(
        view_proj[(0, 0)],
        view_proj[(0, 1)],
        view_proj[(0, 2)],
        view_proj[(0, 3)],
    );
    let row1 = crate::ecs::world::Vec4::new(
        view_proj[(1, 0)],
        view_proj[(1, 1)],
        view_proj[(1, 2)],
        view_proj[(1, 3)],
    );
    let row2 = crate::ecs::world::Vec4::new(
        view_proj[(2, 0)],
        view_proj[(2, 1)],
        view_proj[(2, 2)],
        view_proj[(2, 3)],
    );
    let row3 = crate::ecs::world::Vec4::new(
        view_proj[(3, 0)],
        view_proj[(3, 1)],
        view_proj[(3, 2)],
        view_proj[(3, 3)],
    );

    planes[0] = row3 + row0;
    planes[1] = row3 - row0;
    planes[2] = row3 + row1;
    planes[3] = row3 - row1;
    planes[4] = row2;
    planes[5] = row3 - row2;

    for plane in &mut planes {
        let normal_length = (plane.x * plane.x + plane.y * plane.y + plane.z * plane.z).sqrt();
        if normal_length > 1e-6 {
            *plane /= normal_length;
        }
    }

    planes
}

pub fn sphere_in_frustum(
    center: &crate::ecs::world::Vec3,
    radius: f32,
    frustum_planes: &[crate::ecs::world::Vec4; 6],
) -> bool {
    for plane in frustum_planes {
        let distance = plane.x * center.x + plane.y * center.y + plane.z * center.z + plane.w;
        if distance < -radius {
            return false;
        }
    }
    true
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightData {
    pub position: [f32; 4],
    pub direction: [f32; 4],
    pub color: [f32; 4],
    pub light_type: u32,
    pub range: f32,
    pub inner_cone: f32,
    pub outer_cone: f32,
    pub shadow_index: i32,
    pub light_size: f32,
    pub cookie_layer: u32,
    pub _padding: f32,
}

pub const COOKIE_LAYER_NONE: u32 = 0xFFFFFFFF;

pub use crate::render::wgpu::passes::shadow_depth::{
    CASCADE_SPLIT_DISTANCES, scale_cascade_splits,
};

pub const MAX_SPOTLIGHT_SHADOWS: usize = 64;

pub struct LightCollectionResult {
    pub lights_data: Vec<LightData>,
    pub directional_light: Option<(
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )>,
    pub num_directional_lights: u32,
    pub entity_to_index: std::collections::HashMap<crate::ecs::world::Entity, usize>,
}

pub fn collect_lights(
    world: &crate::ecs::world::World,
    max_lights: usize,
) -> LightCollectionResult {
    let mut directional_lights = Vec::new();
    let mut local_lights = Vec::new();
    let mut directional_light = None;
    let mut directional_entities = Vec::new();
    let mut local_entities = Vec::new();

    for entity in world
        .core
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) = (
            world.core.get_light(entity),
            world.core.get_global_transform(entity),
        ) {
            if light.light_type == crate::ecs::light::components::LightType::Area {
                continue;
            }

            let light_type = match light.light_type {
                crate::ecs::light::components::LightType::Directional => 0,
                crate::ecs::light::components::LightType::Point => 1,
                crate::ecs::light::components::LightType::Spot => 2,
                crate::ecs::light::components::LightType::Area => continue,
            };

            if light_type == 0 && light.cast_shadows && directional_light.is_none() {
                directional_light = Some((light.clone(), *transform));
            }

            let position = transform.translation();
            let direction = transform.forward_vector();

            let light_size = match light.light_type {
                crate::ecs::light::components::LightType::Directional => 1.0,
                crate::ecs::light::components::LightType::Point => (light.range * 0.05).max(0.1),
                crate::ecs::light::components::LightType::Spot => (light.range * 0.03).max(0.1),
                crate::ecs::light::components::LightType::Area => 0.1,
            };

            let light_data = LightData {
                position: [position.x, position.y, position.z, 1.0],
                direction: [direction.x, direction.y, direction.z, 0.0],
                color: [
                    light.color.x * light.intensity,
                    light.color.y * light.intensity,
                    light.color.z * light.intensity,
                    1.0,
                ],
                light_type,
                range: light.range,
                inner_cone: light.inner_cone_angle.cos(),
                outer_cone: light.outer_cone_angle.cos(),
                shadow_index: -1,
                light_size,
                cookie_layer: COOKIE_LAYER_NONE,
                _padding: 0.0,
            };

            if light_type == 0 {
                directional_lights.push(light_data);
                directional_entities.push(entity);
            } else {
                local_lights.push(light_data);
                local_entities.push(entity);
            }

            if directional_lights.len() + local_lights.len() >= max_lights {
                break;
            }
        }
    }

    let num_directional_lights = directional_lights.len() as u32;
    let mut lights_data = directional_lights;
    lights_data.extend(local_lights);

    let mut entity_to_index = std::collections::HashMap::new();
    for (index, entity) in directional_entities.iter().enumerate() {
        entity_to_index.insert(*entity, index);
    }
    let offset = directional_entities.len();
    for (index, entity) in local_entities.iter().enumerate() {
        entity_to_index.insert(*entity, offset + index);
    }

    LightCollectionResult {
        lights_data,
        directional_light,
        num_directional_lights,
        entity_to_index,
    }
}

/// Maximum number of area lights shaded per fragment. Area lights are few and
/// expensive, so they are iterated directly rather than clustered.
pub const MAX_AREA_LIGHTS: usize = 16;

/// Sentinel emissive-texture layer meaning "no texture" for an area light.
pub const AREA_EMISSIVE_LAYER_NONE: u32 = 0xFFFFFFFF;

const AREA_SHAPE_RECTANGLE: u32 = 0;
const AREA_SHAPE_DISK: u32 = 1;
const AREA_SHAPE_SPHERE: u32 = 2;
const AREA_SHAPE_TUBE: u32 = 3;

/// GPU representation of an area light. The emitter normal (emission direction)
/// is packed into the w lanes of position/right/up to keep the struct at 96
/// bytes with 16-byte alignment.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AreaLightData {
    pub position: [f32; 4],
    pub right: [f32; 4],
    pub up: [f32; 4],
    pub color: [f32; 4],
    pub shape: u32,
    pub range: f32,
    pub radius: f32,
    pub two_sided: u32,
    pub shadow_index: i32,
    pub emissive_layer: u32,
    pub _pad0: f32,
    pub _pad1: f32,
}

/// Uniform paired with the area-light storage buffer carrying the live count.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AreaUniforms {
    pub count: u32,
    pub _pad0: u32,
    pub _pad1: u32,
    pub _pad2: u32,
}

pub struct AreaLightCollectionResult {
    pub area_lights_data: Vec<AreaLightData>,
    pub entity_to_index: std::collections::HashMap<crate::ecs::world::Entity, usize>,
}

pub fn collect_area_lights(
    world: &crate::ecs::world::World,
    max_area_lights: usize,
) -> AreaLightCollectionResult {
    let mut area_lights_data = Vec::new();
    let mut entity_to_index = std::collections::HashMap::new();

    for entity in world
        .core
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if area_lights_data.len() >= max_area_lights {
            break;
        }

        let (Some(light), Some(transform)) = (
            world.core.get_light(entity),
            world.core.get_global_transform(entity),
        ) else {
            continue;
        };

        if light.light_type != crate::ecs::light::components::LightType::Area {
            continue;
        }

        let center = transform.translation();
        let right_dir = transform.right_vector();
        let up_dir = transform.up_vector();
        let normal = transform.forward_vector();

        let (right_half, up_half, radius) = match light.area_shape {
            crate::ecs::light::components::AreaLightShape::Rectangle => (
                right_dir * (light.area_width * 0.5),
                up_dir * (light.area_height * 0.5),
                (light.area_width + light.area_height) * 0.1,
            ),
            crate::ecs::light::components::AreaLightShape::Disk => (
                right_dir * light.area_radius,
                up_dir * light.area_radius,
                light.area_radius,
            ),
            crate::ecs::light::components::AreaLightShape::Sphere => (
                right_dir * light.area_radius,
                up_dir * light.area_radius,
                light.area_radius,
            ),
            crate::ecs::light::components::AreaLightShape::Tube => (
                right_dir * (light.area_width * 0.5),
                up_dir * light.area_radius,
                light.area_radius,
            ),
        };

        let shape = match light.area_shape {
            crate::ecs::light::components::AreaLightShape::Rectangle => AREA_SHAPE_RECTANGLE,
            crate::ecs::light::components::AreaLightShape::Disk => AREA_SHAPE_DISK,
            crate::ecs::light::components::AreaLightShape::Sphere => AREA_SHAPE_SPHERE,
            crate::ecs::light::components::AreaLightShape::Tube => AREA_SHAPE_TUBE,
        };

        entity_to_index.insert(entity, area_lights_data.len());
        area_lights_data.push(AreaLightData {
            position: [center.x, center.y, center.z, normal.x],
            right: [right_half.x, right_half.y, right_half.z, normal.y],
            up: [up_half.x, up_half.y, up_half.z, normal.z],
            color: [
                light.color.x * light.intensity,
                light.color.y * light.intensity,
                light.color.z * light.intensity,
                1.0,
            ],
            shape,
            range: light.range,
            radius,
            two_sided: u32::from(light.area_two_sided),
            shadow_index: -1,
            emissive_layer: AREA_EMISSIVE_LAYER_NONE,
            _pad0: 0.0,
            _pad1: 0.0,
        });
    }

    AreaLightCollectionResult {
        area_lights_data,
        entity_to_index,
    }
}

pub fn apply_area_light_shadow_indices(
    area_lights_data: &mut [AreaLightData],
    entity_to_shadow_index: &std::collections::HashMap<crate::ecs::world::Entity, i32>,
    entity_to_area_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
) {
    for (entity, &shadow_index) in entity_to_shadow_index {
        if let Some(&area_index) = entity_to_area_index.get(entity)
            && area_index < area_lights_data.len()
        {
            area_lights_data[area_index].shadow_index = shadow_index;
        }
    }
}

pub struct CascadeShadowResult {
    pub cascade_view_projections: [[[f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub cascade_diameters: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub cascade_split_distances: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub light_view_projection: [[f32; 4]; 4],
    pub shadow_bias: f32,
    pub shadows_enabled: f32,
}

pub fn calculate_cascade_shadows(
    world: &crate::ecs::world::World,
    directional_light: Option<&(
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )>,
) -> CascadeShadowResult {
    let mut cascade_view_projections =
        [[[0.0f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];
    let mut cascade_diameters = [0.0f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];
    let mut cascade_split_distances = CASCADE_SPLIT_DISTANCES;

    let (light_view_projection, shadow_bias, shadows_enabled) =
        if let Some((_light, light_transform)) = directional_light {
            let light_direction = light_transform.forward_vector();

            let camera_matrices = crate::ecs::camera::queries::query_active_camera_matrices(world);

            if let Some(camera_matrices) = camera_matrices {
                let active_camera = world.resources.active_camera;
                let camera = active_camera.and_then(|entity| world.core.get_camera(entity));

                let (fov, aspect, camera_near) = if let Some(camera) = camera {
                    match &camera.projection {
                        crate::ecs::camera::components::Projection::Perspective(persp) => {
                            let aspect = persp.aspect_ratio.unwrap_or_else(|| {
                                crate::ecs::camera::queries::query_window_aspect_ratio(world)
                                    .unwrap_or(1.78)
                            });
                            (persp.y_fov_rad, aspect, persp.z_near)
                        }
                        crate::ecs::camera::components::Projection::Orthographic(_) => {
                            (std::f32::consts::FRAC_PI_4, 1.78, 0.1)
                        }
                    }
                } else {
                    (std::f32::consts::FRAC_PI_4, 1.78, 0.1)
                };

                cascade_split_distances = scale_cascade_splits(camera_near);

                let cascade_resolution = if cfg!(target_arch = "wasm32") {
                    2048.0
                } else {
                    4096.0
                };

                for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
                    let cascade_near = if cascade_index == 0 {
                        camera_near
                    } else {
                        cascade_split_distances[cascade_index - 1]
                    };
                    let cascade_far = cascade_split_distances[cascade_index];

                    let frustum_corners = get_frustum_corners_world_space(
                        &camera_matrices.view,
                        fov,
                        aspect,
                        cascade_near,
                        cascade_far,
                    );

                    let result = calculate_cascade_view_projection(
                        &frustum_corners,
                        &light_direction,
                        cascade_resolution,
                        cascade_far,
                    );

                    cascade_view_projections[cascade_index] = result.view_projection.into();
                    cascade_diameters[cascade_index] = result.cascade_diameter;
                }
            } else {
                let up = if light_direction.y.abs() > 0.99 {
                    nalgebra_glm::vec3(1.0, 0.0, 0.0)
                } else {
                    nalgebra_glm::vec3(0.0, 1.0, 0.0)
                };

                for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
                    let cascade_far = cascade_split_distances[cascade_index];
                    let half_size = cascade_far * 0.5;

                    let scene_center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
                    let light_position = scene_center - light_direction * 100.0;
                    let light_view = nalgebra_glm::look_at(&light_position, &scene_center, &up);
                    let light_projection = reverse_z_ortho_light(
                        -half_size,
                        half_size,
                        -half_size,
                        half_size,
                        0.1,
                        cascade_far * 2.0,
                    );

                    cascade_view_projections[cascade_index] =
                        (light_projection * light_view).into();
                    cascade_diameters[cascade_index] = cascade_far;
                }
            }

            (cascade_view_projections[0], 0.02, 1.0)
        } else {
            (nalgebra_glm::Mat4::identity().into(), 0.0, 0.0)
        };

    CascadeShadowResult {
        cascade_view_projections,
        cascade_diameters,
        cascade_split_distances,
        light_view_projection,
        shadow_bias,
        shadows_enabled,
    }
}

pub fn resolve_cookie_layers(
    world: &crate::ecs::world::World,
    lights_data: &mut [LightData],
    entity_to_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
    material_layer_map: &std::collections::HashMap<
        crate::ecs::asset_id::TextureId,
        crate::render::wgpu::material_texture_arrays::MaterialTextureLayer,
    >,
    texture_registry: &crate::ecs::generational_registry::GenerationalRegistry<
        crate::render::wgpu::texture_cache::TextureEntry,
    >,
) {
    for (entity, &index) in entity_to_index {
        let Some(light) = world.core.get_light(*entity) else {
            continue;
        };
        let Some(cookie_name) = light.cookie_texture.as_deref() else {
            continue;
        };
        if cookie_name.is_empty() {
            continue;
        }
        let Some((texture_index, _)) =
            crate::ecs::generational_registry::registry_lookup_index(texture_registry, cookie_name)
        else {
            continue;
        };
        let texture_id = crate::ecs::asset_id::TextureId::new(texture_index, 0);
        if let Some(entry) = material_layer_map.get(&texture_id)
            && let Some(slot) = lights_data.get_mut(index)
        {
            slot.cookie_layer = entry.packed();
        }
    }
}

/// Resolves each area light's emissive texture path to a bindless material
/// layer, mirroring [`resolve_cookie_layers`]. The texture must already be
/// registered as a material texture. Lights without a texture keep the
/// `AREA_EMISSIVE_LAYER_NONE` sentinel.
pub fn resolve_area_emissive_layers(
    world: &crate::ecs::world::World,
    area_lights_data: &mut [AreaLightData],
    entity_to_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
    material_layer_map: &std::collections::HashMap<
        crate::ecs::asset_id::TextureId,
        crate::render::wgpu::material_texture_arrays::MaterialTextureLayer,
    >,
    texture_registry: &crate::ecs::generational_registry::GenerationalRegistry<
        crate::render::wgpu::texture_cache::TextureEntry,
    >,
) {
    for (entity, &index) in entity_to_index {
        let Some(light) = world.core.get_light(*entity) else {
            continue;
        };
        let Some(texture_name) = light.area_emissive_texture.as_deref() else {
            continue;
        };
        if texture_name.is_empty() {
            continue;
        }
        let Some((texture_index, _)) = crate::ecs::generational_registry::registry_lookup_index(
            texture_registry,
            texture_name,
        ) else {
            continue;
        };
        let texture_id = crate::ecs::asset_id::TextureId::new(texture_index, 0);
        if let Some(entry) = material_layer_map.get(&texture_id)
            && let Some(slot) = area_lights_data.get_mut(index)
        {
            slot.emissive_layer = entry.packed();
        }
    }
}

pub struct SpotlightShadowResult {
    pub shadow_data: Vec<crate::render::wgpu::passes::SpotlightShadowData>,
    pub entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32>,
}

/// Whether a light is shadowed through the spotlight depth atlas. Spotlights
/// always are. Area lights are when their emitter has a fixed normal, which
/// rules out spheres and tubes. Both the sampling-data collection and the
/// atlas render pass call this so they assign the same lights to the same slots.
pub fn light_casts_atlas_shadow(light: &crate::ecs::light::components::Light) -> bool {
    match light.light_type {
        crate::ecs::light::components::LightType::Spot => true,
        crate::ecs::light::components::LightType::Area => matches!(
            light.area_shape,
            crate::ecs::light::components::AreaLightShape::Rectangle
                | crate::ecs::light::components::AreaLightShape::Disk
        ),
        _ => false,
    }
}

/// Field of view for a light's atlas shadow projection. Area lights emit across
/// a wide hemisphere, so they use a fixed wide angle instead of a cone angle.
pub fn atlas_shadow_fov(light: &crate::ecs::light::components::Light) -> f32 {
    match light.light_type {
        crate::ecs::light::components::LightType::Area => 2.4,
        _ => light.outer_cone_angle * 2.0,
    }
}

/// Collects the spotlight and area-light shadow sampling data and entity to
/// slot mapping. This delegates to the same atlas assignment the shadow depth
/// pass renders from, so the sampled slots always match the rendered slots.
pub fn collect_spotlight_shadows(world: &crate::ecs::world::World) -> SpotlightShadowResult {
    let assignment = &world.resources.shadow_atlas;
    SpotlightShadowResult {
        shadow_data: assignment.data.clone(),
        entity_to_shadow_index: assignment.entity_to_index.clone(),
    }
}

pub fn apply_spotlight_shadow_indices(
    lights_data: &mut [LightData],
    entity_to_shadow_index: &std::collections::HashMap<crate::ecs::world::Entity, i32>,
    entity_to_lights_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
) {
    for (entity, &shadow_index) in entity_to_shadow_index {
        if let Some(&lights_index) = entity_to_lights_index.get(entity)
            && lights_index < lights_data.len()
        {
            lights_data[lights_index].shadow_index = shadow_index;
        }
    }
}

pub const MAX_POINT_LIGHT_SHADOWS: usize = 16;

pub fn collect_point_light_shadows(
    world: &crate::ecs::world::World,
    camera_position: nalgebra_glm::Vec3,
    lights_data: &mut [LightData],
    entity_to_lights_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
) -> Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> {
    let mut point_light_candidates: Vec<(
        crate::ecs::world::Entity,
        f32,
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )> = Vec::new();

    for entity in world
        .core
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) = (
            world.core.get_light(entity),
            world.core.get_global_transform(entity),
        ) && matches!(
            light.light_type,
            crate::ecs::light::components::LightType::Point
        ) && light.cast_shadows
        {
            let light_pos = nalgebra_glm::vec3(
                transform.0[(0, 3)],
                transform.0[(1, 3)],
                transform.0[(2, 3)],
            );
            let distance_sq = nalgebra_glm::length2(&(light_pos - camera_position));
            point_light_candidates.push((entity, distance_sq, light.clone(), *transform));
        }
    }

    point_light_candidates
        .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));

    let mut shadow_data: Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> =
        Vec::new();

    for (slot_index, (entity, _distance, light, transform)) in point_light_candidates
        .iter()
        .take(MAX_POINT_LIGHT_SHADOWS)
        .enumerate()
    {
        let light_position = nalgebra_glm::vec3(
            transform.0[(0, 3)],
            transform.0[(1, 3)],
            transform.0[(2, 3)],
        );

        shadow_data.push(
            crate::render::wgpu::passes::shadow_depth::PointLightShadowData {
                position: [light_position.x, light_position.y, light_position.z],
                range: light.range.max(0.1),
                bias: light.shadow_bias,
                shadow_index: slot_index as i32,
                _padding: [0.0; 2],
            },
        );

        if let Some(&lights_index) = entity_to_lights_index.get(entity)
            && lights_index < lights_data.len()
        {
            lights_data[lights_index].shadow_index = slot_index as i32;
        }
    }

    shadow_data
}
