//! Solari GI path integration and capability detection.

use bevy::camera::CameraMainTextureUsages;
use bevy::core_pipeline::prepass::{
    DeferredPrepass, DeferredPrepassDoubleBuffer, DepthPrepass, DepthPrepassDoubleBuffer,
    MotionVectorPrepass,
};
use bevy::mesh::{morph::MeshMorphWeights, skinning::SkinnedMesh, Indices, PrimitiveTopology};
use bevy::pbr::{DefaultOpaqueRendererMethod, ExtractedDirectionalLight};
use bevy::prelude::*;
use bevy::render::render_resource::TextureUsages;
use bevy::render::renderer::{RenderAdapter, RenderDevice};
use bevy::render::{Render, RenderApp, RenderSystems};
use bevy_solari::prelude::{RaytracingMesh3d, SolariLighting};
use bevy_solari::scene::RaytracingSceneBindings;
use bevy_solari::SolariPlugins;
use blacksite_surface::{SurfaceEvaluatorRegistry, SurfaceMaterial};
use settings::{GiPath, ProjectSettings, RenderingCapabilities};
use shared::{LevelObject, RaytracingExcluded};
use std::sync::{Arc, Mutex};

#[derive(Resource, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct SolariRaytracingSceneStats {
    pub project_meshes: usize,
    pub tagged_meshes: usize,
    pub excluded_meshes: usize,
    pub unsupported_meshes: usize,
    /// Skinned or morphed meshes deliberately omitted until Solari consumes deformed vertices.
    pub deformed_meshes_excluded: usize,
    pub surface_material_meshes: usize,
    pub surface_materials_missing_evaluator: usize,
    pub missing_level_root_meshes: usize,
    pub emissive_meshes: usize,
    pub compatible_mesh_assets: usize,
    pub incompatible_mesh_assets: usize,
    pub mesh_assets_missing_tangents: usize,
    pub mesh_assets_missing_u32_indices: usize,
    pub render_instances: usize,
    pub render_directional_lights: usize,
    pub render_bind_group_ready: bool,
}

impl SolariRaytracingSceneStats {
    pub fn compatible_light_count(&self) -> usize {
        self.render_directional_lights + self.emissive_meshes
    }

    pub fn solari_ready(&self) -> bool {
        self.render_bind_group_ready
            && self.render_instances > 0
            && self.compatible_light_count() > 0
    }
}

#[derive(Debug, Default, Clone, Copy)]
struct SolariRenderSceneStats {
    render_instances: usize,
    render_directional_lights: usize,
    render_bind_group_ready: bool,
}

#[derive(Resource, Debug, Clone, Default)]
struct SolariRenderSceneReadback(Arc<Mutex<SolariRenderSceneStats>>);

pub struct SolariRenderingPlugin;

impl Plugin for SolariRenderingPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(blacksite_surface::SurfaceMaterialPlugin)
            .add_plugins(SolariPlugins)
            .init_resource::<RenderingCapabilities>()
            .init_resource::<settings::EffectiveRenderStack>()
            .init_resource::<SolariRaytracingSceneStats>()
            .init_resource::<SolariRenderSceneReadback>()
            .init_resource::<settings::ActiveCameraRenderProfile>()
            .init_resource::<settings::ActiveVolumeContribution>()
            .add_systems(Startup, init_opaque_renderer_method)
            .add_systems(
                Update,
                detect_rendering_capabilities
                    .run_if(not(resource_exists::<RenderingCapabilitiesDetected>)),
            )
            .add_systems(
                PostUpdate,
                (
                    super::volumes::resolve_active_camera_render_profile_system,
                    sync_render_solari_scene_readback,
                    sync_opaque_renderer_method,
                    sync_auxiliary_camera_deferred_prepass,
                    sync_hydrated_raytracing_meshes,
                )
                    .chain(),
            );
    }

    fn finish(&self, app: &mut App) {
        let readback = app.world().resource::<SolariRenderSceneReadback>().clone();
        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
            return;
        };
        render_app.insert_resource(readback).add_systems(
            Render,
            write_render_solari_scene_readback
                .in_set(RenderSystems::Prepare)
                .after(RenderSystems::PrepareBindGroups),
        );
    }
}

/// Set once GPU features have been probed via [`RenderDevice`].
#[derive(Resource, Default)]
struct RenderingCapabilitiesDetected;

fn detect_rendering_capabilities(
    render_device: Option<Res<RenderDevice>>,
    render_adapter: Option<Res<RenderAdapter>>,
    mut caps: ResMut<RenderingCapabilities>,
    mut commands: Commands,
) {
    let Some(render_device) = render_device else {
        return;
    };

    let rt_supported = probe_solari_rt_support(&render_device);
    caps.rt_supported = rt_supported;
    caps.auto_exposure_supported = render_adapter
        .as_deref()
        .map(super::auto_exposure::probe_auto_exposure_support)
        .unwrap_or(false);
    caps.requested_gi_path = GiPath::Forward;
    caps.effective_gi_path = GiPath::Forward;
    caps.active_gi_path = GiPath::Forward;
    caps.fallback_reason = None;
    if rt_supported {
        info!("Rendering: Solari RT features available when GiMode permits");
    } else {
        warn!("Rendering: Solari RT features unavailable — GiMode::Auto uses forward PBR");
    }
    if caps.auto_exposure_supported {
        info!("Rendering: auto exposure (compute) available");
    } else {
        warn!("Rendering: auto exposure unavailable — using manual EV100 fallback");
    }
    commands.insert_resource(RenderingCapabilitiesDetected);
}

/// Returns whether the active GPU exposes all wgpu features required by [`SolariPlugins`].
pub fn probe_solari_rt_support(render_device: &RenderDevice) -> bool {
    if let Ok(value) = std::env::var("BEVY_FPS_FORCE_SOLARI") {
        return !matches!(
            value.trim().to_ascii_lowercase().as_str(),
            "0" | "false" | "off" | "no"
        );
    }
    let required = SolariPlugins::required_wgpu_features();
    render_device.features().contains(required)
}

fn init_opaque_renderer_method(
    settings: Res<ProjectSettings>,
    caps: Res<RenderingCapabilities>,
    mut commands: Commands,
) {
    let requested = settings::resolve_gi_path(settings.rendering.gi_mode, &caps);
    commands.insert_resource(opaque_renderer_method_for(requested));
}

fn sync_opaque_renderer_method(
    caps: Res<RenderingCapabilities>,
    stats: Res<SolariRaytracingSceneStats>,
    mut commands: Commands,
) {
    if !caps.is_changed() && !stats.is_changed() {
        return;
    }
    commands.insert_resource(opaque_renderer_method_for(caps.effective_gi_path));
}

fn opaque_renderer_method_for(path: GiPath) -> DefaultOpaqueRendererMethod {
    match path {
        GiPath::SolariDeferred => DefaultOpaqueRendererMethod::deferred(),
        GiPath::Forward => DefaultOpaqueRendererMethod::forward(),
    }
}

/// Non-primary 3D cameras still need deferred prepass targets when Solari is active.
fn sync_auxiliary_camera_deferred_prepass(
    caps: Res<RenderingCapabilities>,
    mut commands: Commands,
    cameras: Query<
        Entity,
        (
            With<Camera3d>,
            Without<SolariLighting>,
            Without<DeferredPrepass>,
        ),
    >,
) {
    if caps.effective_gi_path != GiPath::SolariDeferred {
        return;
    }
    for entity in &cameras {
        commands.entity(entity).insert((
            DeferredPrepass,
            DepthPrepass,
            MotionVectorPrepass,
            DeferredPrepassDoubleBuffer,
            DepthPrepassDoubleBuffer,
        ));
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "Bevy systems expose independent render assets and scene queries"
)]
fn sync_hydrated_raytracing_meshes(
    caps: Res<RenderingCapabilities>,
    mut commands: Commands,
    mut stats: ResMut<SolariRaytracingSceneStats>,
    materials: Res<Assets<StandardMaterial>>,
    surface_materials: Res<Assets<SurfaceMaterial>>,
    surface_evaluators: Res<SurfaceEvaluatorRegistry>,
    mesh_assets: Res<Assets<Mesh>>,
    meshes: Query<(
        Entity,
        &Mesh3d,
        Option<&MeshMaterial3d<StandardMaterial>>,
        Option<&MeshMaterial3d<SurfaceMaterial>>,
        Option<&SkinnedMesh>,
        Option<&MeshMorphWeights>,
        Option<&RaytracingMesh3d>,
        Option<&RaytracingExcluded>,
    )>,
    raytraced: Query<Entity, With<RaytracingMesh3d>>,
    parents: Query<&ChildOf>,
    level_roots: Query<(), With<LevelObject>>,
) {
    let mut next_stats = *stats;
    next_stats.project_meshes = 0;
    next_stats.tagged_meshes = 0;
    next_stats.excluded_meshes = 0;
    next_stats.unsupported_meshes = 0;
    next_stats.deformed_meshes_excluded = 0;
    next_stats.surface_material_meshes = 0;
    next_stats.surface_materials_missing_evaluator = 0;
    next_stats.missing_level_root_meshes = 0;
    next_stats.emissive_meshes = 0;
    next_stats.compatible_mesh_assets = 0;
    next_stats.incompatible_mesh_assets = 0;
    next_stats.mesh_assets_missing_tangents = 0;
    next_stats.mesh_assets_missing_u32_indices = 0;

    if caps.requested_gi_path != GiPath::SolariDeferred {
        for entity in &raytraced {
            commands.entity(entity).remove::<RaytracingMesh3d>();
        }
        next_stats.render_instances = 0;
        next_stats.render_directional_lights = 0;
        next_stats.render_bind_group_ready = false;
        if *stats != next_stats {
            info!(
                "Solari raytracing scene disabled: requested={:?}; removing {} raytracing mesh tags",
                caps.requested_gi_path,
                raytraced.iter().len()
            );
            *stats = next_stats;
        }
        return;
    }

    for (entity, mesh, standard_material, surface_material, skin, morph, rt_mesh, excluded) in
        &meshes
    {
        if excluded.is_some() {
            next_stats.excluded_meshes += 1;
            if rt_mesh.is_some() {
                commands.entity(entity).remove::<RaytracingMesh3d>();
            }
            continue;
        }

        if !has_level_object_ancestor(entity, &parents, &level_roots) {
            next_stats.missing_level_root_meshes += 1;
            continue;
        }

        next_stats.project_meshes += 1;

        // Bevy 0.19's mesh allocator exposes the source vertex slab to Solari, not the
        // morph-then-skin output consumed by raster rendering. Tagging this mesh would trace the
        // bind pose, so omit it until the fork owns an instance-keyed deformed vertex/BLAS path.
        let mesh_has_morph_targets = mesh_assets
            .get(&mesh.0)
            .and_then(Mesh::get_morph_targets)
            .is_some();
        if !solari_deformation_is_supported(skin.is_some(), morph.is_some(), mesh_has_morph_targets)
        {
            next_stats.deformed_meshes_excluded += 1;
            if rt_mesh.is_some() {
                commands.entity(entity).remove::<RaytracingMesh3d>();
            }
            continue;
        }

        if standard_material.is_none() && surface_material.is_none() {
            next_stats.unsupported_meshes += 1;
            if rt_mesh.is_some() {
                commands.entity(entity).remove::<RaytracingMesh3d>();
            }
            continue;
        }

        if let Some(surface_handle) = surface_material {
            next_stats.surface_material_meshes += 1;
            let Some(material) = surface_materials.get(&surface_handle.0) else {
                next_stats.unsupported_meshes += 1;
                if rt_mesh.is_some() {
                    commands.entity(entity).remove::<RaytracingMesh3d>();
                }
                continue;
            };
            if !solari_alpha_mode_is_supported(material.base.alpha_mode) {
                next_stats.unsupported_meshes += 1;
                if rt_mesh.is_some() {
                    commands.entity(entity).remove::<RaytracingMesh3d>();
                }
                continue;
            }
            if !surface_evaluators
                .evaluators
                .contains_key(&material.extension.uniform.shader_id)
            {
                next_stats.surface_materials_missing_evaluator += 1;
                next_stats.unsupported_meshes += 1;
                if rt_mesh.is_some() {
                    commands.entity(entity).remove::<RaytracingMesh3d>();
                }
                continue;
            }
            // A custom evaluator may emit light as a function of parameters, textures, or world
            // position. Conservatively register Surface instances as potential emissive sources;
            // zero-emission samples contribute no energy.
            next_stats.emissive_meshes += 1;
        } else if let Some(standard_handle) = standard_material {
            let Some(material) = materials.get(&standard_handle.0) else {
                next_stats.unsupported_meshes += 1;
                if rt_mesh.is_some() {
                    commands.entity(entity).remove::<RaytracingMesh3d>();
                }
                continue;
            };
            if !solari_alpha_mode_is_supported(material.alpha_mode) {
                next_stats.unsupported_meshes += 1;
                if rt_mesh.is_some() {
                    commands.entity(entity).remove::<RaytracingMesh3d>();
                }
                continue;
            }
            if material.emissive.to_vec3() != Vec3::ZERO {
                next_stats.emissive_meshes += 1;
            }
        }

        match mesh_assets
            .get(&mesh.0)
            .map(classify_solari_mesh_compatibility)
        {
            Some(SolariMeshCompatibility::Compatible) => {
                next_stats.compatible_mesh_assets += 1;
            }
            Some(SolariMeshCompatibility::MissingTangents) => {
                next_stats.incompatible_mesh_assets += 1;
                next_stats.mesh_assets_missing_tangents += 1;
            }
            Some(SolariMeshCompatibility::MissingU32Indices) => {
                next_stats.incompatible_mesh_assets += 1;
                next_stats.mesh_assets_missing_u32_indices += 1;
            }
            Some(SolariMeshCompatibility::Other) | None => {
                next_stats.incompatible_mesh_assets += 1;
            }
        }

        next_stats.tagged_meshes += 1;
        let needs_insert = rt_mesh.is_none_or(|rt_mesh| rt_mesh.0 != mesh.0);
        if needs_insert {
            commands
                .entity(entity)
                .insert(RaytracingMesh3d(mesh.0.clone()));
        }
    }

    for entity in &raytraced {
        if !mesh_is_solari_project_geometry(entity, &meshes, &parents, &level_roots) {
            commands.entity(entity).remove::<RaytracingMesh3d>();
        }
    }

    if *stats != next_stats {
        info!(
            "Solari mesh eligibility: requested={:?} eligible={} tagged={} compatible_assets={} incompatible_assets={} missing_tangents={} missing_u32_indices={} unsupported={} deformed_excluded={} surface_materials={} surface_missing_evaluator={} excluded={} outside_level_roots={} emissive_candidates={}",
            caps.requested_gi_path,
            next_stats.project_meshes,
            next_stats.tagged_meshes,
            next_stats.compatible_mesh_assets,
            next_stats.incompatible_mesh_assets,
            next_stats.mesh_assets_missing_tangents,
            next_stats.mesh_assets_missing_u32_indices,
            next_stats.unsupported_meshes,
            next_stats.deformed_meshes_excluded,
            next_stats.surface_material_meshes,
            next_stats.surface_materials_missing_evaluator,
            next_stats.excluded_meshes,
            next_stats.missing_level_root_meshes,
            next_stats.emissive_meshes,
        );
        *stats = next_stats;
    }
}

fn solari_deformation_is_supported(
    has_skin: bool,
    has_morph_weights: bool,
    mesh_has_morph_targets: bool,
) -> bool {
    !has_skin && !has_morph_weights && !mesh_has_morph_targets
}

fn solari_alpha_mode_is_supported(alpha_mode: AlphaMode) -> bool {
    matches!(
        alpha_mode,
        AlphaMode::Opaque | AlphaMode::Mask(_) | AlphaMode::AlphaToCoverage
    )
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SolariMeshCompatibility {
    Compatible,
    MissingTangents,
    MissingU32Indices,
    Other,
}

fn classify_solari_mesh_compatibility(mesh: &Mesh) -> SolariMeshCompatibility {
    if mesh.primitive_topology() != PrimitiveTopology::TriangleList || !mesh.enable_raytracing {
        return SolariMeshCompatibility::Other;
    }
    let attribute_ids: Vec<_> = mesh
        .attributes()
        .map(|(attribute, _)| attribute.id)
        .collect();
    let expected = [
        Mesh::ATTRIBUTE_POSITION.id,
        Mesh::ATTRIBUTE_NORMAL.id,
        Mesh::ATTRIBUTE_UV_0.id,
        Mesh::ATTRIBUTE_TANGENT.id,
    ];
    if attribute_ids != expected {
        if !attribute_ids.contains(&Mesh::ATTRIBUTE_TANGENT.id) {
            return SolariMeshCompatibility::MissingTangents;
        }
        return SolariMeshCompatibility::Other;
    }
    if !matches!(mesh.indices(), Some(Indices::U32(_))) {
        return SolariMeshCompatibility::MissingU32Indices;
    }
    SolariMeshCompatibility::Compatible
}

fn mesh_is_solari_project_geometry(
    entity: Entity,
    meshes: &Query<(
        Entity,
        &Mesh3d,
        Option<&MeshMaterial3d<StandardMaterial>>,
        Option<&MeshMaterial3d<SurfaceMaterial>>,
        Option<&SkinnedMesh>,
        Option<&MeshMorphWeights>,
        Option<&RaytracingMesh3d>,
        Option<&RaytracingExcluded>,
    )>,
    parents: &Query<&ChildOf>,
    level_roots: &Query<(), With<LevelObject>>,
) -> bool {
    let Ok((_, _, standard_material, surface_material, skin, morph, _, excluded)) =
        meshes.get(entity)
    else {
        return false;
    };
    excluded.is_none()
        && skin.is_none()
        && morph.is_none()
        && (standard_material.is_some() || surface_material.is_some())
        && has_level_object_ancestor(entity, parents, level_roots)
}

fn has_level_object_ancestor(
    mut entity: Entity,
    parents: &Query<&ChildOf>,
    level_roots: &Query<(), With<LevelObject>>,
) -> bool {
    const MAX_DEPTH: usize = 64;
    for _ in 0..MAX_DEPTH {
        if level_roots.contains(entity) {
            return true;
        }
        let Ok(parent) = parents.get(entity) else {
            return false;
        };
        entity = parent.parent();
    }
    false
}

fn sync_render_solari_scene_readback(
    caps: Res<RenderingCapabilities>,
    readback: Res<SolariRenderSceneReadback>,
    mut stats: ResMut<SolariRaytracingSceneStats>,
) {
    if caps.requested_gi_path != GiPath::SolariDeferred {
        let mut next = *stats;
        next.render_instances = 0;
        next.render_directional_lights = 0;
        next.render_bind_group_ready = false;
        if *stats != next {
            *stats = next;
        }
        return;
    }

    let Ok(render_stats) = readback.0.lock() else {
        return;
    };
    let mut next = *stats;
    next.render_instances = render_stats.render_instances;
    next.render_directional_lights = render_stats.render_directional_lights;
    next.render_bind_group_ready = render_stats.render_bind_group_ready;
    if *stats != next {
        let was_ready = stats.solari_ready();
        let is_ready = next.solari_ready();
        if was_ready != is_ready
            || stats.render_instances != next.render_instances
            || stats.render_directional_lights != next.render_directional_lights
            || stats.render_bind_group_ready != next.render_bind_group_ready
        {
            info!(
                "Solari render readback: ready={} instances={} directional_lights={} bind_group_ready={} compatible_lights={}",
                is_ready,
                next.render_instances,
                next.render_directional_lights,
                next.render_bind_group_ready,
                next.compatible_light_count(),
            );
        }
        *stats = next;
    }
}

fn write_render_solari_scene_readback(
    readback: Res<SolariRenderSceneReadback>,
    scene_bindings: Option<Res<RaytracingSceneBindings>>,
    raytracing_instances: Query<(), With<RaytracingMesh3d>>,
    directional_lights: Query<(), With<ExtractedDirectionalLight>>,
) {
    let Ok(mut stats) = readback.0.lock() else {
        return;
    };
    *stats = SolariRenderSceneStats {
        render_instances: raytracing_instances.iter().len(),
        render_directional_lights: directional_lights.iter().len(),
        render_bind_group_ready: scene_bindings
            .is_some_and(|bindings| bindings.bind_group.is_some()),
    };
}

pub fn effective_gi_path_for_camera(
    requested: GiPath,
    stats: Option<&SolariRaytracingSceneStats>,
) -> GiPath {
    let _ = stats;
    requested
}

pub fn sync_gi_path(commands: &mut Commands, entity: Entity, path: GiPath) {
    match path {
        GiPath::SolariDeferred => {
            commands.entity(entity).insert((
                SolariLighting::default(),
                CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
            ));
        }
        GiPath::Forward => {
            commands
                .entity(entity)
                .remove::<(SolariLighting, CameraMainTextureUsages)>();
        }
    }
}

pub fn strip_gi_path(commands: &mut Commands, entity: Entity) {
    sync_gi_path(commands, entity, GiPath::Forward);
}

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::ecs::system::SystemState;

    #[test]
    fn level_object_descendant_is_project_geometry() {
        let mut world = World::new();
        let root = world.spawn(LevelObject).id();
        let child = world.spawn(ChildOf(root)).id();

        let mut state: SystemState<(Query<&ChildOf>, Query<(), With<LevelObject>>)> =
            SystemState::new(&mut world);
        let (parents, level_roots) = state
            .get(&world)
            .expect("solari test system params should be valid");

        assert!(has_level_object_ancestor(child, &parents, &level_roots));
        assert!(has_level_object_ancestor(root, &parents, &level_roots));
    }

    #[test]
    fn orphan_mesh_is_not_project_geometry() {
        let mut world = World::new();
        let orphan = world.spawn_empty().id();

        let mut state: SystemState<(Query<&ChildOf>, Query<(), With<LevelObject>>)> =
            SystemState::new(&mut world);
        let (parents, level_roots) = state
            .get(&world)
            .expect("solari test system params should be valid");

        assert!(!has_level_object_ancestor(orphan, &parents, &level_roots));
    }

    #[test]
    fn solari_request_can_start_before_render_scene_is_ready() {
        let stats = SolariRaytracingSceneStats {
            project_meshes: 1,
            tagged_meshes: 1,
            render_instances: 1,
            render_directional_lights: 1,
            render_bind_group_ready: false,
            ..default()
        };

        assert_eq!(
            effective_gi_path_for_camera(GiPath::SolariDeferred, Some(&stats)),
            GiPath::SolariDeferred
        );
    }

    #[test]
    fn solari_request_uses_solari_when_render_scene_is_ready() {
        let stats = SolariRaytracingSceneStats {
            project_meshes: 1,
            tagged_meshes: 1,
            render_instances: 1,
            render_directional_lights: 1,
            render_bind_group_ready: true,
            ..default()
        };

        assert_eq!(
            effective_gi_path_for_camera(GiPath::SolariDeferred, Some(&stats)),
            GiPath::SolariDeferred
        );
    }

    #[test]
    fn deformation_is_excluded_instead_of_tracing_the_bind_pose() {
        assert!(solari_deformation_is_supported(false, false, false));
        assert!(!solari_deformation_is_supported(true, false, false));
        assert!(!solari_deformation_is_supported(false, true, false));
        assert!(!solari_deformation_is_supported(false, false, true));
        assert!(!solari_deformation_is_supported(true, true, true));
    }

    #[test]
    fn solari_accepts_only_opaque_and_cutout_materials() {
        assert!(solari_alpha_mode_is_supported(AlphaMode::Opaque));
        assert!(solari_alpha_mode_is_supported(AlphaMode::Mask(0.5)));
        assert!(solari_alpha_mode_is_supported(AlphaMode::AlphaToCoverage));
        assert!(!solari_alpha_mode_is_supported(AlphaMode::Blend));
        assert!(!solari_alpha_mode_is_supported(AlphaMode::Premultiplied));
    }
}
