//! Assembles the high-fidelity rendering stack on project cameras.

use bevy::anti_alias::taa::TemporalAntiAliasing;
use bevy::camera::{Exposure, Hdr};
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::light::{atmosphere::ScatteringMedium, Atmosphere};
use bevy::pbr::{AtmosphereSettings, ScreenSpaceAmbientOcclusion};
use bevy::post_process::auto_exposure::AutoExposure;
use bevy::post_process::bloom::Bloom;
use bevy::prelude::*;
use settings::{
    effective_auto_exposure, ActiveCameraRenderProfile, GiPath, ProjectRenderCamera,
    ProjectSettings, RenderingCapabilities, RenderingSettings,
};

use super::auto_exposure::auto_exposure_from_profile;
use sim::PlayerCamera;

use super::fullscreen_effects::{strip_fullscreen_effects, sync_fullscreen_effect};
use super::solari::{strip_gi_path, sync_gi_path, SolariRaytracingSceneStats};
use super::viewport_camera::effective_viewport_gi_path;

/// Marker for the single project-owned atmosphere entity used by Bevy 0.19 cameras.
#[derive(Component, Debug, Clone, Copy)]
pub struct ProjectAtmosphere;

/// Environment variable name for HDR override (matches game launch).
pub const HDR_ENV_VAR: &str = "BEVY_FPS_HDR";

fn hdr_enabled_from_env() -> bool {
    std::env::var(HDR_ENV_VAR)
        .map(|value| {
            !matches!(
                value.trim().to_ascii_lowercase().as_str(),
                "0" | "false" | "off" | "no"
            )
        })
        .unwrap_or(true)
}

/// True when the camera should run auto exposure this frame (mode, HDR pass, compute).
pub fn camera_auto_exposure_active(
    profile: &ActiveCameraRenderProfile,
    caps: &RenderingCapabilities,
) -> bool {
    effective_auto_exposure(profile, caps) && hdr_enabled_profile(profile)
}

/// Whether the HDR render pass is active for this profile (respects `BEVY_FPS_HDR`).
pub fn hdr_enabled_profile(profile: &ActiveCameraRenderProfile) -> bool {
    if std::env::var(HDR_ENV_VAR).is_ok() {
        hdr_enabled_from_env()
    } else {
        profile.hdr
    }
}

/// True when the camera render target must be HDR for authored HDR, atmosphere sky, or Solari storage writes.
pub fn hdr_required_for_effective_gi(
    profile: &ActiveCameraRenderProfile,
    effective_gi: GiPath,
) -> bool {
    hdr_required_for_render_stack(
        hdr_enabled_profile(profile),
        profile.atmosphere,
        effective_gi,
    )
}

pub fn hdr_required_for_render_stack(
    hdr_enabled: bool,
    atmosphere_enabled: bool,
    effective_gi: GiPath,
) -> bool {
    hdr_enabled || atmosphere_enabled || effective_gi == GiPath::SolariDeferred
}

/// Applies the shared project rendering profile to all [`ProjectRenderCamera`] entities.
#[unsafe(no_mangle)]
#[expect(
    clippy::too_many_arguments,
    reason = "Bevy systems expose independent render resources and light queries"
)]
pub fn setup_project_camera_effects(
    mut commands: Commands,
    mut mediums: ResMut<Assets<ScatteringMedium>>,
    settings: Res<ProjectSettings>,
    profile: Option<Res<ActiveCameraRenderProfile>>,
    caps: Option<Res<RenderingCapabilities>>,
    solari_stats: Option<Res<SolariRaytracingSceneStats>>,
    cameras: Query<Entity, With<ProjectRenderCamera>>,
    atmospheres: Query<Entity, With<ProjectAtmosphere>>,
    points: Query<&PointLight>,
    spots: Query<&SpotLight>,
) {
    let default_caps = RenderingCapabilities::default();
    let gi_caps = caps.as_deref().unwrap_or(&default_caps);
    let active = profile.map(|p| p.clone()).unwrap_or_else(|| {
        settings::ActiveCameraRenderProfile::from_project(
            &settings.rendering,
            settings::resolve_gi_path(settings.rendering.gi_mode, gi_caps),
        )
    });
    let has_local_shadow_lights = super::viewport_camera::has_local_shadow_lights(&points, &spots);
    sync_project_atmosphere(
        &mut commands,
        &mut mediums,
        active.atmosphere,
        atmospheres.iter(),
    );
    for entity in &cameras {
        apply_camera_render_profile(
            &mut commands,
            entity,
            &active,
            gi_caps,
            solari_stats.as_deref(),
            &mut mediums,
            has_local_shadow_lights,
        );
    }
}

/// Keeps the Bevy 0.19 atmosphere model stable: one world entity, camera settings per view.
pub fn sync_project_atmosphere(
    commands: &mut Commands,
    mediums: &mut Assets<ScatteringMedium>,
    enabled: bool,
    atmospheres: impl IntoIterator<Item = Entity>,
) {
    let existing: Vec<Entity> = atmospheres.into_iter().collect();
    if enabled {
        if existing.is_empty() {
            let medium = mediums.add(ScatteringMedium::default());
            commands.spawn((ProjectAtmosphere, Atmosphere::earth(medium)));
        }
        for entity in existing.iter().skip(1) {
            commands.entity(*entity).despawn();
        }
    } else {
        for entity in existing {
            commands.entity(entity).despawn();
        }
    }
}

/// Inserts or removes manual [`Exposure`] vs [`AutoExposure`] for the active profile.
pub fn sync_exposure(
    commands: &mut Commands,
    entity: Entity,
    profile: &ActiveCameraRenderProfile,
    caps: &RenderingCapabilities,
) {
    if camera_auto_exposure_active(profile, caps) {
        commands
            .entity(entity)
            .remove::<Exposure>()
            .insert(auto_exposure_from_profile(profile));
    } else {
        commands
            .entity(entity)
            .remove::<AutoExposure>()
            .insert(Exposure {
                ev100: profile.exposure_ev100,
            });
    }
}

/// Inserts post-processing on a camera entity from a resolved render profile.
pub fn apply_camera_render_profile(
    commands: &mut Commands,
    entity: Entity,
    profile: &ActiveCameraRenderProfile,
    caps: &RenderingCapabilities,
    solari_stats: Option<&SolariRaytracingSceneStats>,
    _mediums: &mut Assets<ScatteringMedium>,
    has_local_shadow_lights: bool,
) {
    commands.entity(entity).insert(Msaa::Off);
    let effective_gi =
        effective_viewport_gi_path(profile.gi_path, caps, solari_stats, has_local_shadow_lights);
    sync_exposure(commands, entity, profile, caps);
    commands.entity(entity).insert(DistanceFog {
        color: Color::srgb(
            profile.fog_color[0],
            profile.fog_color[1],
            profile.fog_color[2],
        ),
        falloff: FogFalloff::Exponential {
            density: profile.fog_density,
        },
        ..default()
    });

    if profile.atmosphere {
        commands
            .entity(entity)
            .remove::<Atmosphere>()
            .insert(AtmosphereSettings::default());
    } else {
        commands
            .entity(entity)
            .remove::<Atmosphere>()
            .remove::<AtmosphereSettings>();
    }
    if profile.tonemapping_aces {
        commands.entity(entity).insert(Tonemapping::AcesFitted);
    }
    if profile.bloom {
        commands.entity(entity).insert(Bloom::NATURAL);
    }
    if profile.ssao {
        commands
            .entity(entity)
            .insert(ScreenSpaceAmbientOcclusion::default());
    }
    if profile.taa {
        commands
            .entity(entity)
            .insert(TemporalAntiAliasing::default());
    }
    if hdr_required_for_effective_gi(profile, effective_gi) {
        commands.entity(entity).insert(Hdr);
    }

    sync_gi_path(commands, entity, effective_gi);
    sync_fullscreen_effect(commands, entity, profile.fullscreen_effect.as_deref());
}

/// Legacy entry: applies project settings directly (no volume blend).
pub fn apply_project_camera_fx(
    commands: &mut Commands,
    entity: Entity,
    rendering: &RenderingSettings,
    mediums: &mut Assets<ScatteringMedium>,
) {
    let profile = ActiveCameraRenderProfile::from_project(rendering, settings::GiPath::Forward);
    apply_camera_render_profile(
        commands,
        entity,
        &profile,
        &RenderingCapabilities::default(),
        None,
        mediums,
        false,
    );
}

/// Removes the high-cost post-processing stack from a camera entity.
pub fn strip_project_camera_fx(commands: &mut Commands, entity: Entity) {
    strip_fullscreen_effects(commands, entity);
    strip_gi_path(commands, entity);
    commands
        .entity(entity)
        .remove::<ProjectRenderCamera>()
        .remove::<Atmosphere>()
        .remove::<AtmosphereSettings>()
        .remove::<TemporalAntiAliasing>()
        .remove::<ScreenSpaceAmbientOcclusion>()
        .remove::<Bloom>()
        .remove::<Hdr>()
        .remove::<Tonemapping>()
        .remove::<Exposure>()
        .remove::<AutoExposure>()
        .remove::<DistanceFog>()
        .remove::<Msaa>();
}

/// Marks the player camera for the shared rendering stack.
#[unsafe(no_mangle)]
pub fn tag_player_camera(
    mut commands: Commands,
    cameras: Query<Entity, (With<PlayerCamera>, Without<ProjectRenderCamera>)>,
) {
    for entity in &cameras {
        commands.entity(entity).insert(ProjectRenderCamera);
    }
}

/// Toggles optional post-processing components without stripping the whole stack.
#[allow(clippy::too_many_arguments)]
pub fn sync_optional_rendering_fx(
    commands: &mut Commands,
    entity: Entity,
    profile: &ActiveCameraRenderProfile,
    caps: &RenderingCapabilities,
    solari_stats: Option<&SolariRaytracingSceneStats>,
    _mediums: &mut Assets<ScatteringMedium>,
    has_atmosphere_settings: bool,
    has_tonemapping: bool,
    has_bloom: bool,
    has_ssao: bool,
    has_taa: bool,
    has_hdr: bool,
    has_auto_exposure: bool,
    has_local_shadow_lights: bool,
) {
    let effective_gi =
        effective_viewport_gi_path(profile.gi_path, caps, solari_stats, has_local_shadow_lights);

    if profile.atmosphere && !has_atmosphere_settings {
        commands
            .entity(entity)
            .remove::<Atmosphere>()
            .insert(AtmosphereSettings::default());
    } else if !profile.atmosphere && has_atmosphere_settings {
        commands
            .entity(entity)
            .remove::<Atmosphere>()
            .remove::<AtmosphereSettings>();
    } else if profile.atmosphere {
        commands.entity(entity).remove::<Atmosphere>();
    }

    if profile.tonemapping_aces && !has_tonemapping {
        commands.entity(entity).insert(Tonemapping::AcesFitted);
    } else if !profile.tonemapping_aces && has_tonemapping {
        commands.entity(entity).remove::<Tonemapping>();
    }

    if profile.bloom && !has_bloom {
        commands.entity(entity).insert(Bloom::NATURAL);
    } else if !profile.bloom && has_bloom {
        commands.entity(entity).remove::<Bloom>();
    }

    if profile.ssao && !has_ssao {
        commands
            .entity(entity)
            .insert(ScreenSpaceAmbientOcclusion::default());
    } else if !profile.ssao && has_ssao {
        commands
            .entity(entity)
            .remove::<ScreenSpaceAmbientOcclusion>();
    }

    if profile.taa && !has_taa {
        commands
            .entity(entity)
            .insert(TemporalAntiAliasing::default());
    } else if !profile.taa && has_taa {
        commands.entity(entity).remove::<TemporalAntiAliasing>();
    }

    let wants_hdr = hdr_required_for_effective_gi(profile, effective_gi);
    if wants_hdr && !has_hdr {
        commands.entity(entity).insert(Hdr);
    } else if !wants_hdr && has_hdr {
        commands.entity(entity).remove::<Hdr>();
    }

    let wants_auto = camera_auto_exposure_active(profile, caps);
    if wants_auto != has_auto_exposure {
        sync_exposure(commands, entity, profile, caps);
    } else if wants_auto {
        commands
            .entity(entity)
            .insert(auto_exposure_from_profile(profile));
    } else {
        commands.entity(entity).insert(Exposure {
            ev100: profile.exposure_ev100,
        });
    }

    sync_gi_path(commands, entity, effective_gi);
    sync_fullscreen_effect(commands, entity, profile.fullscreen_effect.as_deref());
}

/// Patches exposure/fog/HDR when the active profile changes at runtime.
pub fn patch_camera_effects(
    profile: &ActiveCameraRenderProfile,
    caps: &RenderingCapabilities,
    solari_stats: Option<&SolariRaytracingSceneStats>,
    has_local_shadow_lights: bool,
    commands: &mut Commands,
    cameras: impl IntoIterator<Item = Entity>,
) {
    for entity in cameras {
        let effective_gi = effective_viewport_gi_path(
            profile.gi_path,
            caps,
            solari_stats,
            has_local_shadow_lights,
        );
        sync_exposure(commands, entity, profile, caps);
        commands.entity(entity).insert(DistanceFog {
            color: Color::srgb(
                profile.fog_color[0],
                profile.fog_color[1],
                profile.fog_color[2],
            ),
            falloff: FogFalloff::Exponential {
                density: profile.fog_density,
            },
            ..default()
        });
        if hdr_required_for_effective_gi(profile, effective_gi) {
            commands.entity(entity).insert(Hdr);
        } else {
            commands.entity(entity).remove::<Hdr>();
        }
        sync_gi_path(commands, entity, effective_gi);
        sync_fullscreen_effect(commands, entity, profile.fullscreen_effect.as_deref());
    }
}

/// Re-applies player camera FX tags after a hot reload.
pub fn refresh_player_camera_fx(world: &mut World) {
    let Some(settings) = world.get_resource::<ProjectSettings>().cloned() else {
        return;
    };
    let profile = world
        .get_resource::<ActiveCameraRenderProfile>()
        .cloned()
        .unwrap_or_else(|| {
            ActiveCameraRenderProfile::from_project(&settings.rendering, settings::GiPath::Forward)
        });
    let solari_stats = world.get_resource::<SolariRaytracingSceneStats>().copied();
    let caps = world
        .get_resource::<RenderingCapabilities>()
        .cloned()
        .unwrap_or_default();
    let has_local_shadow_lights = super::viewport_camera::world_has_local_shadow_lights(world);
    let cameras: Vec<Entity> = world
        .query_filtered::<Entity, With<PlayerCamera>>()
        .iter(world)
        .collect();

    for entity in &cameras {
        if world.get::<ProjectRenderCamera>(*entity).is_none() {
            world.commands().entity(*entity).insert(ProjectRenderCamera);
        }
    }

    world.resource_scope(|world, mut mediums: Mut<Assets<ScatteringMedium>>| {
        let atmospheres: Vec<Entity> = world
            .query_filtered::<Entity, With<ProjectAtmosphere>>()
            .iter(world)
            .collect();
        sync_project_atmosphere(
            &mut world.commands(),
            &mut mediums,
            profile.atmosphere,
            atmospheres,
        );
        for entity in cameras {
            apply_camera_render_profile(
                &mut world.commands(),
                entity,
                &profile,
                &caps,
                solari_stats.as_ref(),
                &mut mediums,
                has_local_shadow_lights,
            );
        }
    });
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn solari_forces_hdr_render_target() {
        assert!(hdr_required_for_render_stack(
            false,
            false,
            GiPath::SolariDeferred
        ));
        assert!(hdr_required_for_render_stack(true, false, GiPath::Forward));
        assert!(!hdr_required_for_render_stack(
            false,
            false,
            GiPath::Forward
        ));
    }

    #[test]
    fn atmosphere_forces_hdr_render_target() {
        assert!(hdr_required_for_render_stack(false, true, GiPath::Forward));
    }
}
