//! Light probes for baked global illumination.

use bevy_app::{App, Plugin};
use bevy_asset::AssetId;
use bevy_camera::{
    primitives::{Aabb, Frustum},
    Camera3d,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
    component::Component,
    entity::Entity,
    query::With,
    resource::Resource,
    schedule::IntoScheduleConfigs,
    system::{Commands, Local, Query, Res, ResMut},
};
use bevy_image::Image;
use bevy_light::{EnvironmentMapLight, IrradianceVolume, LightProbe};
use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4};
use bevy_platform::collections::HashMap;
use crate::render::{
    extract_instances::ExtractInstancesPlugin,
    render_asset::RenderAssets,
    render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView},
    renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper},
    settings::WgpuFeatures,
    sync_world::RenderEntity,
    texture::{FallbackImage, GpuImage},
    view::ExtractedView,
    Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
};
use bevy_shader::load_shader_library;
use bevy_transform::{components::Transform, prelude::GlobalTransform};
use tracing::error;

use core::{hash::Hash, ops::Deref};

use crate::render::pbr::{
    generate::EnvironmentMapGenerationPlugin, light_probe::environment_map::EnvironmentMapIds,
};

pub mod environment_map;
pub mod generate;
pub mod irradiance_volume;

/// The maximum number of each type of light probe that each view will consider.
///
/// Because the fragment shader does a linear search through the list for each
/// fragment, this number needs to be relatively small.
pub const MAX_VIEW_LIGHT_PROBES: usize = 8;

/// How many texture bindings are used in the fragment shader, *not* counting
/// environment maps or irradiance volumes.
const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16;

/// Adds support for light probes: cuboid bounding regions that apply global
/// illumination to objects within them.
///
/// This also adds support for view environment maps: diffuse and specular
/// cubemaps applied to all objects that a view renders.
pub struct LightProbePlugin;

/// A GPU type that stores information about a light probe.
#[derive(Clone, Copy, ShaderType, Default)]
struct RenderLightProbe {
    /// The transform from the world space to the model space. This is used to
    /// efficiently check for bounding box intersection.
    light_from_world_transposed: [Vec4; 3],

    /// The index of the texture or textures in the appropriate binding array or
    /// arrays.
    ///
    /// For example, for reflection probes this is the index of the cubemap in
    /// the diffuse and specular texture arrays.
    texture_index: i32,

    /// Scale factor applied to the light generated by this light probe.
    ///
    /// See the comment in [`EnvironmentMapLight`] for details.
    intensity: f32,

    /// Whether this light probe adds to the diffuse contribution of the
    /// irradiance for meshes with lightmaps.
    affects_lightmapped_mesh_diffuse: u32,
}

/// A per-view shader uniform that specifies all the light probes that the view
/// takes into account.
#[derive(ShaderType)]
pub struct LightProbesUniform {
    /// The list of applicable reflection probes, sorted from nearest to the
    /// camera to the farthest away from the camera.
    reflection_probes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],

    /// The list of applicable irradiance volumes, sorted from nearest to the
    /// camera to the farthest away from the camera.
    irradiance_volumes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],

    /// The number of reflection probes in the list.
    reflection_probe_count: i32,

    /// The number of irradiance volumes in the list.
    irradiance_volume_count: i32,

    /// The index of the diffuse and specular environment maps associated with
    /// the view itself. This is used as a fallback if no reflection probe in
    /// the list contains the fragment.
    view_cubemap_index: i32,

    /// The smallest valid mipmap level for the specular environment cubemap
    /// associated with the view.
    smallest_specular_mip_level_for_view: u32,

    /// The intensity of the environment cubemap associated with the view.
    ///
    /// See the comment in [`EnvironmentMapLight`] for details.
    intensity_for_view: f32,

    /// Whether the environment map attached to the view affects the diffuse
    /// lighting for lightmapped meshes.
    ///
    /// This will be 1 if the map does affect lightmapped meshes or 0 otherwise.
    view_environment_map_affects_lightmapped_mesh_diffuse: u32,
}

/// A GPU buffer that stores information about all light probes.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct LightProbesBuffer(DynamicUniformBuffer<LightProbesUniform>);

/// A component attached to each camera in the render world that stores the
/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`].
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewLightProbesUniformOffset(u32);

/// Information that [`gather_light_probes`] keeps about each light probe.
///
/// This information is parameterized by the [`LightProbeComponent`] type. This
/// will either be [`EnvironmentMapLight`] for reflection probes or
/// [`IrradianceVolume`] for irradiance volumes.
struct LightProbeInfo<C>
where
    C: LightProbeComponent,
{
    // The transform from world space to light probe space.
    // Stored as the transpose of the inverse transform to compress the structure
    // on the GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it
    // to recover the original inverse transform.
    light_from_world: [Vec4; 3],

    // The transform from light probe space to world space.
    world_from_light: Affine3A,

    // Scale factor applied to the diffuse and specular light generated by this
    // reflection probe.
    //
    // See the comment in [`EnvironmentMapLight`] for details.
    intensity: f32,

    // Whether this light probe adds to the diffuse contribution of the
    // irradiance for meshes with lightmaps.
    affects_lightmapped_mesh_diffuse: bool,

    // The IDs of all assets associated with this light probe.
    //
    // Because each type of light probe component may reference different types
    // of assets (e.g. a reflection probe references two cubemap assets while an
    // irradiance volume references a single 3D texture asset), this is generic.
    asset_id: C::AssetId,
}

/// A component, part of the render world, that stores the mapping from asset ID
/// or IDs to the texture index in the appropriate binding arrays.
///
/// Cubemap textures belonging to environment maps are collected into binding
/// arrays, and the index of each texture is presented to the shader for runtime
/// lookup. 3D textures belonging to reflection probes are likewise collected
/// into binding arrays, and the shader accesses the 3D texture by index.
///
/// This component is attached to each view in the render world, because each
/// view may have a different set of light probes that it considers and therefore
/// the texture indices are per-view.
#[derive(Component, Default)]
pub struct RenderViewLightProbes<C>
where
    C: LightProbeComponent,
{
    /// The list of environment maps presented to the shader, in order.
    binding_index_to_textures: Vec<C::AssetId>,

    /// The reverse of `binding_index_to_cubemap`: a map from the texture ID to
    /// the index in `binding_index_to_cubemap`.
    cubemap_to_binding_index: HashMap<C::AssetId, u32>,

    /// Information about each light probe, ready for upload to the GPU, sorted
    /// in order from closest to the camera to farthest.
    ///
    /// Note that this is not necessarily ordered by binding index. So don't
    /// write code like
    /// `render_light_probes[cubemap_to_binding_index[asset_id]]`; instead
    /// search for the light probe with the appropriate binding index in this
    /// array.
    render_light_probes: Vec<RenderLightProbe>,

    /// Information needed to render the light probe attached directly to the
    /// view, if applicable.
    ///
    /// A light probe attached directly to a view represents a "global" light
    /// probe that affects all objects not in the bounding region of any light
    /// probe. Currently, the only light probe type that supports this is the
    /// [`EnvironmentMapLight`].
    view_light_probe_info: C::ViewLightProbeInfo,
}

/// A trait implemented by all components that represent light probes.
///
/// Currently, the two light probe types are [`EnvironmentMapLight`] and
/// [`IrradianceVolume`], for reflection probes and irradiance volumes
/// respectively.
///
/// Most light probe systems are written to be generic over the type of light
/// probe. This allows much of the code to be shared and enables easy addition
/// of more light probe types (e.g. real-time reflection planes) in the future.
pub trait LightProbeComponent: Send + Sync + Component + Sized {
    /// Holds [`AssetId`]s of the texture or textures that this light probe
    /// references.
    ///
    /// This can just be [`AssetId`] if the light probe only references one
    /// texture. If it references multiple textures, it will be a structure
    /// containing those asset IDs.
    type AssetId: Send + Sync + Clone + Eq + Hash;

    /// If the light probe can be attached to the view itself (as opposed to a
    /// cuboid region within the scene), this contains the information that will
    /// be passed to the GPU in order to render it. Otherwise, this will be
    /// `()`.
    ///
    /// Currently, only reflection probes (i.e. [`EnvironmentMapLight`]) can be
    /// attached directly to views.
    type ViewLightProbeInfo: Send + Sync + Default;

    /// Returns the asset ID or asset IDs of the texture or textures referenced
    /// by this light probe.
    fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId>;

    /// Returns the intensity of this light probe.
    ///
    /// This is a scaling factor that will be multiplied by the value or values
    /// sampled from the texture.
    fn intensity(&self) -> f32;

    /// Returns true if this light probe contributes diffuse lighting to meshes
    /// with lightmaps or false otherwise.
    fn affects_lightmapped_mesh_diffuse(&self) -> bool;

    /// Creates an instance of [`RenderViewLightProbes`] containing all the
    /// information needed to render this light probe.
    ///
    /// This is called for every light probe in view every frame.
    fn create_render_view_light_probes(
        view_component: Option<&Self>,
        image_assets: &RenderAssets<GpuImage>,
    ) -> RenderViewLightProbes<Self>;
}

/// The uniform struct extracted from [`EnvironmentMapLight`].
/// Will be available for use in the Environment Map shader.
#[derive(Component, ShaderType, Clone)]
pub struct EnvironmentMapUniform {
    /// The world space transformation matrix of the sample ray for environment cubemaps.
    transform: Mat4,
}

impl Default for EnvironmentMapUniform {
    fn default() -> Self {
        EnvironmentMapUniform {
            transform: Mat4::IDENTITY,
        }
    }
}

/// A GPU buffer that stores the environment map settings for each view.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer<EnvironmentMapUniform>);

/// A component that stores the offset within the
/// [`EnvironmentMapUniformBuffer`] for each view.
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewEnvironmentMapUniformOffset(u32);

impl Plugin for LightProbePlugin {
    fn build(&self, app: &mut App) {
        load_shader_library!(app, "light_probe.wgsl");
        load_shader_library!(app, "environment_map.wgsl");
        load_shader_library!(app, "irradiance_volume.wgsl");

        app.add_plugins((
            EnvironmentMapGenerationPlugin,
            ExtractInstancesPlugin::<EnvironmentMapIds>::new(),
        ));

        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
            return;
        };

        render_app
            .init_resource::<LightProbesBuffer>()
            .init_resource::<EnvironmentMapUniformBuffer>()
            .add_systems(ExtractSchedule, gather_environment_map_uniform)
            .add_systems(ExtractSchedule, gather_light_probes::<EnvironmentMapLight>)
            .add_systems(ExtractSchedule, gather_light_probes::<IrradianceVolume>)
            .add_systems(
                Render,
                (upload_light_probes, prepare_environment_uniform_buffer)
                    .in_set(RenderSystems::PrepareResources),
            );
    }
}

/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them.
///
/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance
/// if one does not already exist.
fn gather_environment_map_uniform(
    view_query: Extract<Query<(RenderEntity, Option<&EnvironmentMapLight>), With<Camera3d>>>,
    mut commands: Commands,
) {
    for (view_entity, environment_map_light) in view_query.iter() {
        let environment_map_uniform = if let Some(environment_map_light) = environment_map_light {
            EnvironmentMapUniform {
                transform: Transform::from_rotation(environment_map_light.rotation)
                    .to_matrix()
                    .inverse(),
            }
        } else {
            EnvironmentMapUniform::default()
        };
        commands
            .get_entity(view_entity)
            .expect("Environment map light entity wasn't synced.")
            .insert(environment_map_uniform);
    }
}

/// Gathers up all light probes of a single type in the scene and assigns them
/// to views, performing frustum culling and distance sorting in the process.
fn gather_light_probes<C>(
    image_assets: Res<RenderAssets<GpuImage>>,
    light_probe_query: Extract<Query<(&GlobalTransform, &C), With<LightProbe>>>,
    view_query: Extract<
        Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With<Camera3d>>,
    >,
    mut reflection_probes: Local<Vec<LightProbeInfo<C>>>,
    mut view_reflection_probes: Local<Vec<LightProbeInfo<C>>>,
    mut commands: Commands,
) where
    C: LightProbeComponent,
{
    // Create [`LightProbeInfo`] for every light probe in the scene.
    reflection_probes.clear();
    reflection_probes.extend(
        light_probe_query
            .iter()
            .filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)),
    );
    // Build up the light probes uniform and the key table.
    for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() {
        // Cull light probes outside the view frustum.
        view_reflection_probes.clear();
        view_reflection_probes.extend(
            reflection_probes
                .iter()
                .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum))
                .cloned(),
        );

        // Sort by distance to camera.
        view_reflection_probes.sort_by_cached_key(|light_probe_info| {
            light_probe_info.camera_distance_sort_key(view_transform)
        });

        // Create the light probes list.
        let mut render_view_light_probes =
            C::create_render_view_light_probes(view_component, &image_assets);

        // Gather up the light probes in the list.
        render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes);

        // Record the per-view light probes.
        if render_view_light_probes.is_empty() {
            commands
                .get_entity(view_entity)
                .expect("View entity wasn't synced.")
                .remove::<RenderViewLightProbes<C>>();
        } else {
            commands
                .get_entity(view_entity)
                .expect("View entity wasn't synced.")
                .insert(render_view_light_probes);
        }
    }
}

/// Gathers up environment map settings for each applicable view and
/// writes them into a GPU buffer.
pub fn prepare_environment_uniform_buffer(
    mut commands: Commands,
    views: Query<(Entity, Option<&EnvironmentMapUniform>), With<ExtractedView>>,
    mut environment_uniform_buffer: ResMut<EnvironmentMapUniformBuffer>,
    render_device: Res<RenderDevice>,
    render_queue: Res<RenderQueue>,
) {
    let Some(mut writer) =
        environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
    else {
        return;
    };

    for (view, environment_uniform) in views.iter() {
        let uniform_offset = match environment_uniform {
            None => 0,
            Some(environment_uniform) => writer.write(environment_uniform),
        };
        commands
            .entity(view)
            .insert(ViewEnvironmentMapUniformOffset(uniform_offset));
    }
}

// A system that runs after [`gather_light_probes`] and populates the GPU
// uniforms with the results.
//
// Note that, unlike [`gather_light_probes`], this system is not generic over
// the type of light probe. It collects light probes of all types together into
// a single structure, ready to be passed to the shader.
fn upload_light_probes(
    mut commands: Commands,
    views: Query<Entity, With<ExtractedView>>,
    mut light_probes_buffer: ResMut<LightProbesBuffer>,
    mut view_light_probes_query: Query<(
        Option<&RenderViewLightProbes<EnvironmentMapLight>>,
        Option<&RenderViewLightProbes<IrradianceVolume>>,
    )>,
    render_device: Res<RenderDevice>,
    render_queue: Res<RenderQueue>,
) {
    // If there are no views, bail.
    if views.is_empty() {
        return;
    }

    // Initialize the uniform buffer writer.
    let mut writer = light_probes_buffer
        .get_writer(views.iter().len(), &render_device, &render_queue)
        .unwrap();

    // Process each view.
    for view_entity in views.iter() {
        let Ok((render_view_environment_maps, render_view_irradiance_volumes)) =
            view_light_probes_query.get_mut(view_entity)
        else {
            error!("Failed to find `RenderViewLightProbes` for the view!");
            continue;
        };

        // Initialize the uniform with only the view environment map, if there
        // is one.
        let mut light_probes_uniform = LightProbesUniform {
            reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
            irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
            reflection_probe_count: render_view_environment_maps
                .map(RenderViewLightProbes::len)
                .unwrap_or_default()
                .min(MAX_VIEW_LIGHT_PROBES) as i32,
            irradiance_volume_count: render_view_irradiance_volumes
                .map(RenderViewLightProbes::len)
                .unwrap_or_default()
                .min(MAX_VIEW_LIGHT_PROBES) as i32,
            view_cubemap_index: render_view_environment_maps
                .map(|maps| maps.view_light_probe_info.cubemap_index)
                .unwrap_or(-1),
            smallest_specular_mip_level_for_view: render_view_environment_maps
                .map(|maps| maps.view_light_probe_info.smallest_specular_mip_level)
                .unwrap_or(0),
            intensity_for_view: render_view_environment_maps
                .map(|maps| maps.view_light_probe_info.intensity)
                .unwrap_or(1.0),
            view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps
                .map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32)
                .unwrap_or(1),
        };

        // Add any environment maps that [`gather_light_probes`] found to the
        // uniform.
        if let Some(render_view_environment_maps) = render_view_environment_maps {
            render_view_environment_maps.add_to_uniform(
                &mut light_probes_uniform.reflection_probes,
                &mut light_probes_uniform.reflection_probe_count,
            );
        }

        // Add any irradiance volumes that [`gather_light_probes`] found to the
        // uniform.
        if let Some(render_view_irradiance_volumes) = render_view_irradiance_volumes {
            render_view_irradiance_volumes.add_to_uniform(
                &mut light_probes_uniform.irradiance_volumes,
                &mut light_probes_uniform.irradiance_volume_count,
            );
        }

        // Queue the view's uniforms to be written to the GPU.
        let uniform_offset = writer.write(&light_probes_uniform);

        commands
            .entity(view_entity)
            .insert(ViewLightProbesUniformOffset(uniform_offset));
    }
}

impl Default for LightProbesUniform {
    fn default() -> Self {
        Self {
            reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
            irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
            reflection_probe_count: 0,
            irradiance_volume_count: 0,
            view_cubemap_index: -1,
            smallest_specular_mip_level_for_view: 0,
            intensity_for_view: 1.0,
            view_environment_map_affects_lightmapped_mesh_diffuse: 1,
        }
    }
}

impl<C> LightProbeInfo<C>
where
    C: LightProbeComponent,
{
    /// Given the set of light probe components, constructs and returns
    /// [`LightProbeInfo`]. This is done for every light probe in the scene
    /// every frame.
    fn new(
        (light_probe_transform, environment_map): (&GlobalTransform, &C),
        image_assets: &RenderAssets<GpuImage>,
    ) -> Option<LightProbeInfo<C>> {
        let light_from_world_transposed =
            Mat4::from(light_probe_transform.affine().inverse()).transpose();
        environment_map.id(image_assets).map(|id| LightProbeInfo {
            world_from_light: light_probe_transform.affine(),
            light_from_world: [
                light_from_world_transposed.x_axis,
                light_from_world_transposed.y_axis,
                light_from_world_transposed.z_axis,
            ],
            asset_id: id,
            intensity: environment_map.intensity(),
            affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(),
        })
    }

    /// Returns true if this light probe is in the viewing frustum of the camera
    /// or false if it isn't.
    fn frustum_cull(&self, view_frustum: &Frustum) -> bool {
        view_frustum.intersects_obb(
            &Aabb {
                center: Vec3A::default(),
                half_extents: Vec3A::splat(0.5),
            },
            &self.world_from_light,
            true,
            false,
        )
    }

    /// Returns the squared distance from this light probe to the camera,
    /// suitable for distance sorting.
    fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd {
        FloatOrd(
            (self.world_from_light.translation - view_transform.translation_vec3a())
                .length_squared(),
        )
    }
}

impl<C> RenderViewLightProbes<C>
where
    C: LightProbeComponent,
{
    /// Creates a new empty list of light probes.
    fn new() -> RenderViewLightProbes<C> {
        RenderViewLightProbes {
            binding_index_to_textures: vec![],
            cubemap_to_binding_index: HashMap::default(),
            render_light_probes: vec![],
            view_light_probe_info: C::ViewLightProbeInfo::default(),
        }
    }

    /// Returns true if there are no light probes in the list.
    pub(crate) fn is_empty(&self) -> bool {
        self.binding_index_to_textures.is_empty()
    }

    /// Returns the number of light probes in the list.
    pub(crate) fn len(&self) -> usize {
        self.binding_index_to_textures.len()
    }

    /// Adds a cubemap to the list of bindings, if it wasn't there already, and
    /// returns its index within that list.
    pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &C::AssetId) -> u32 {
        *self
            .cubemap_to_binding_index
            .entry((*cubemap_id).clone())
            .or_insert_with(|| {
                let index = self.binding_index_to_textures.len() as u32;
                self.binding_index_to_textures.push((*cubemap_id).clone());
                index
            })
    }

    /// Adds all the light probes in this structure to the supplied array, which
    /// is expected to be shipped to the GPU.
    fn add_to_uniform(
        &self,
        render_light_probes: &mut [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
        render_light_probe_count: &mut i32,
    ) {
        render_light_probes[0..self.render_light_probes.len()]
            .copy_from_slice(&self.render_light_probes[..]);
        *render_light_probe_count = self.render_light_probes.len() as i32;
    }

    /// Gathers up all light probes of the given type in the scene and records
    /// them in this structure.
    fn maybe_gather_light_probes(&mut self, light_probes: &[LightProbeInfo<C>]) {
        for light_probe in light_probes.iter().take(MAX_VIEW_LIGHT_PROBES) {
            // Determine the index of the cubemap in the binding array.
            let cubemap_index = self.get_or_insert_cubemap(&light_probe.asset_id);

            // Write in the light probe data.
            self.render_light_probes.push(RenderLightProbe {
                light_from_world_transposed: light_probe.light_from_world,
                texture_index: cubemap_index as i32,
                intensity: light_probe.intensity,
                affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse
                    as u32,
            });
        }
    }
}

impl<C> Clone for LightProbeInfo<C>
where
    C: LightProbeComponent,
{
    fn clone(&self) -> Self {
        Self {
            light_from_world: self.light_from_world,
            world_from_light: self.world_from_light,
            intensity: self.intensity,
            affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse,
            asset_id: self.asset_id.clone(),
        }
    }
}

/// Adds a diffuse or specular texture view to the `texture_views` list, and
/// populates `sampler` if this is the first such view.
pub(crate) fn add_cubemap_texture_view<'a>(
    texture_views: &mut Vec<&'a <TextureView as Deref>::Target>,
    sampler: &mut Option<&'a Sampler>,
    image_id: AssetId<Image>,
    images: &'a RenderAssets<GpuImage>,
    fallback_image: &'a FallbackImage,
) {
    match images.get(image_id) {
        None => {
            // Use the fallback image if the cubemap isn't loaded yet.
            texture_views.push(&*fallback_image.cube.texture_view);
        }
        Some(image) => {
            // If this is the first texture view, populate `sampler`.
            if sampler.is_none() {
                *sampler = Some(&image.sampler);
            }

            texture_views.push(&*image.texture_view);
        }
    }
}

/// Many things can go wrong when attempting to use texture binding arrays
/// (a.k.a. bindless textures). This function checks for these pitfalls:
///
/// 1. If GLSL support is enabled at the feature level, then in debug mode
///    `naga_oil` will attempt to compile all shader modules under GLSL to check
///    validity of names, even if GLSL isn't actually used. This will cause a crash
///    if binding arrays are enabled, because binding arrays are currently
///    unimplemented in the GLSL backend of Naga. Therefore, we disable binding
///    arrays if the `shader_format_glsl` feature is present.
///
/// 2. If there aren't enough texture bindings available to accommodate all the
///    binding arrays, the driver will panic. So we also bail out if there aren't
///    enough texture bindings available in the fragment shader.
///
/// 3. If binding arrays aren't supported on the hardware, then we obviously
///    can't use them. Adreno <= 610 claims to support bindless, but seems to be
///    too buggy to be usable.
///
/// 4. If binding arrays are supported on the hardware, but they can only be
///    accessed by uniform indices, that's not good enough, and we bail out.
///
/// If binding arrays aren't usable, we disable reflection probes and limit the
/// number of irradiance volumes in the scene to 1.
pub(crate) fn binding_arrays_are_usable(
    render_device: &RenderDevice,
    render_adapter: &RenderAdapter,
) -> bool {
    let adapter_info = RenderAdapterInfo(WgpuWrapper::new(render_adapter.get_info()));

    !cfg!(feature = "shader_format_glsl")
        && crate::render::get_adreno_model(&adapter_info).is_none_or(|model| model > 610)
        && render_device.limits().max_storage_textures_per_shader_stage
            >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES)
                as u32
        && render_device.features().contains(
            WgpuFeatures::TEXTURE_BINDING_ARRAY
                | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
        )
}
