//! Single owner for the viewport camera post/render stack (edit + play WYSIWYG).

use super::camera_fx::{
    apply_camera_render_profile, camera_auto_exposure_active, hdr_required_for_effective_gi,
    strip_project_camera_fx, sync_exposure, sync_optional_rendering_fx,
};
use super::solari::SolariRaytracingSceneStats;
use bevy::anti_alias::taa::TemporalAntiAliasing;
use bevy::camera::{Exposure, Hdr};
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::light::atmosphere::ScatteringMedium;
use bevy::pbr::{AtmosphereSettings, ScreenSpaceAmbientOcclusion};
use bevy::post_process::auto_exposure::AutoExposure;
use bevy::post_process::bloom::Bloom;
use bevy::prelude::*;
use settings::{
    resolve_effective_render_stack, write_effective_render_stack, ActiveCameraRenderProfile,
    EffectiveRenderStack, GiPath, ProjectRenderCamera, RenderingCapabilities,
};

/// Which camera entity owns the viewport render stack this frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewportCameraOwner {
    EditorFly,
    Player,
}

/// Maps editor mode + play possession to the active viewport camera owner.
pub fn resolve_viewport_camera_owner(
    editing: bool,
    playing_possessed: bool,
) -> ViewportCameraOwner {
    if editing || !playing_possessed {
        ViewportCameraOwner::EditorFly
    } else {
        ViewportCameraOwner::Player
    }
}

/// True when any point or spot light in the world casts shadows.
pub fn has_local_shadow_lights(points: &Query<&PointLight>, spots: &Query<&SpotLight>) -> bool {
    points.iter().any(|l| l.shadow_maps_enabled) || spots.iter().any(|l| l.shadow_maps_enabled)
}

/// [`has_local_shadow_lights`] for exclusive [`World`] access (hot reload, diagnostics).
pub fn world_has_local_shadow_lights(world: &mut World) -> bool {
    for pl in world.query::<&PointLight>().iter(world) {
        if pl.shadow_maps_enabled {
            return true;
        }
    }
    for sl in world.query::<&SpotLight>().iter(world) {
        if sl.shadow_maps_enabled {
            return true;
        }
    }
    false
}

/// Effective GI path for the main viewport.
pub fn effective_viewport_gi_path(
    requested: GiPath,
    caps: &RenderingCapabilities,
    stats: Option<&SolariRaytracingSceneStats>,
    has_local_shadow_lights: bool,
) -> GiPath {
    effective_viewport_render_stack(requested, caps, stats, has_local_shadow_lights)
        .effective_gi_path
}

/// Effective render-stack decision for the main viewport.
pub fn effective_viewport_render_stack(
    requested: GiPath,
    caps: &RenderingCapabilities,
    stats: Option<&SolariRaytracingSceneStats>,
    has_local_shadow_lights: bool,
) -> EffectiveRenderStack {
    resolve_effective_render_stack(
        requested,
        caps,
        stats.is_some_and(SolariRaytracingSceneStats::solari_ready),
        has_local_shadow_lights,
    )
}

/// Removes the full project render stack from a camera (owner handoff or inactive viewport).
pub fn clear_viewport_camera_stack(commands: &mut Commands, entity: Entity) {
    strip_project_camera_fx(commands, entity);
}

/// Snapshot of optional FX components on a camera for incremental sync.
#[derive(Debug, Clone, Copy, Default)]
pub struct ViewportFxSnapshot {
    pub has_stack: bool,
    pub has_atmosphere_settings: bool,
    pub has_tonemapping: bool,
    pub has_bloom: bool,
    pub has_ssao: bool,
    pub has_taa: bool,
    pub has_hdr: bool,
    pub has_auto_exposure: bool,
}

/// How to apply the viewport stack this frame.
#[derive(Debug, Clone, Copy)]
pub enum ViewportStackApply {
    /// Strip was already done or camera is bare — full profile apply.
    Full,
    /// Camera already carries [`ProjectRenderCamera`]; patch deltas only.
    Incremental(ViewportFxSnapshot),
}

/// Applies or updates the viewport render stack on the active camera entity.
#[allow(clippy::too_many_arguments)]
pub fn sync_viewport_camera_stack(
    commands: &mut Commands,
    entity: Entity,
    profile: &ActiveCameraRenderProfile,
    caps: &mut RenderingCapabilities,
    solari_stats: Option<&SolariRaytracingSceneStats>,
    has_local_shadow_lights: bool,
    mediums: &mut Assets<ScatteringMedium>,
    mode: ViewportStackApply,
) {
    let stack = effective_viewport_render_stack(
        profile.gi_path,
        caps,
        solari_stats,
        has_local_shadow_lights,
    );
    write_effective_render_stack(caps, stack);
    commands.insert_resource(stack);
    let effective_gi = stack.effective_gi_path;

    match mode {
        ViewportStackApply::Full => {
            strip_project_camera_fx(commands, entity);
            commands.entity(entity).insert(ProjectRenderCamera);
            apply_camera_render_profile(
                commands,
                entity,
                profile,
                caps,
                solari_stats,
                mediums,
                has_local_shadow_lights,
            );
        }
        ViewportStackApply::Incremental(snap) => {
            commands.entity(entity).insert(ProjectRenderCamera);
            commands.entity(entity).insert(Msaa::Off);
            sync_optional_rendering_fx(
                commands,
                entity,
                profile,
                caps,
                solari_stats,
                mediums,
                snap.has_atmosphere_settings,
                snap.has_tonemapping,
                snap.has_bloom,
                snap.has_ssao,
                snap.has_taa,
                snap.has_hdr,
                snap.has_auto_exposure,
                has_local_shadow_lights,
            );
            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()
            });
            let wants_auto = camera_auto_exposure_active(profile, caps);
            if wants_auto != snap.has_auto_exposure {
                sync_exposure(commands, entity, profile, caps);
            } else if !wants_auto {
                commands.entity(entity).insert(Exposure {
                    ev100: profile.exposure_ev100,
                });
            }
            super::solari::sync_gi_path(commands, entity, effective_gi);
            super::fullscreen_effects::sync_fullscreen_effect(
                commands,
                entity,
                profile.fullscreen_effect.as_deref(),
            );
            let wants_hdr = hdr_required_for_effective_gi(profile, effective_gi);
            if wants_hdr != snap.has_hdr {
                if wants_hdr {
                    commands.entity(entity).insert(Hdr);
                } else {
                    commands.entity(entity).remove::<Hdr>();
                }
            }
        }
    }
}

/// Builds a [`ViewportFxSnapshot`] from an entity's current components.
pub fn snapshot_viewport_fx(entity: Entity, world: &World) -> ViewportFxSnapshot {
    ViewportFxSnapshot {
        has_stack: world.get::<ProjectRenderCamera>(entity).is_some(),
        has_atmosphere_settings: world.get::<AtmosphereSettings>(entity).is_some(),
        has_tonemapping: world.get::<Tonemapping>(entity).is_some(),
        has_bloom: world.get::<Bloom>(entity).is_some(),
        has_ssao: world.get::<ScreenSpaceAmbientOcclusion>(entity).is_some(),
        has_taa: world.get::<TemporalAntiAliasing>(entity).is_some(),
        has_hdr: world.get::<Hdr>(entity).is_some(),
        has_auto_exposure: world.get::<AutoExposure>(entity).is_some(),
    }
}

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

    #[test]
    fn effective_viewport_gi_keeps_solari_when_local_shadow_lights_exist() {
        let caps = RenderingCapabilities {
            rt_supported: true,
            ..Default::default()
        };
        let stats = Some(SolariRaytracingSceneStats {
            render_bind_group_ready: true,
            render_instances: 1,
            render_directional_lights: 1,
            ..Default::default()
        });
        assert_eq!(
            effective_viewport_gi_path(GiPath::SolariDeferred, &caps, stats.as_ref(), true),
            GiPath::SolariDeferred
        );
        assert_eq!(
            effective_viewport_gi_path(GiPath::SolariDeferred, &caps, stats.as_ref(), false),
            GiPath::SolariDeferred
        );
    }
}
