//! Exclusive ownership of the full project PBR stack (WYSIWYG, one active 3D view).
//!
//! Only [`sync_viewport_camera_stack`] mutates post-FX; scene/play systems set render targets only.

use std::collections::HashSet;

use bevy::anti_alias::taa::TemporalAntiAliasing;
use bevy::camera::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 game::rendering::{
    clear_viewport_camera_stack, has_local_shadow_lights, resolve_viewport_camera_owner,
    sync_project_atmosphere, sync_viewport_camera_stack, ProjectAtmosphere,
    SolariRaytracingSceneStats, ViewportCameraOwner, ViewportFxSnapshot, ViewportStackApply,
};
use settings::{
    ActiveCameraRenderProfile, EffectiveRenderStack, GiPath, ProjectRenderCamera, ProjectSettings,
    RenderingCapabilities,
};

use crate::camera::EditorCamera;
use crate::render_target::ViewportRenderTarget;
use crate::state::{EditorMode, PlayPossession};
use sim::PlayerCamera;

/// Updated before [`sync_project_render_view`] so the render-view system stays within Bevy's param limit.
#[derive(Resource, Default, Clone, Copy)]
pub struct ViewportLocalShadowLights(pub bool);

pub struct RenderViewPlugin;

impl Plugin for RenderViewPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<ViewportLocalShadowLights>()
            .add_systems(
                PostUpdate,
                (sync_viewport_local_shadow_lights, sync_project_render_view)
                    .chain()
                    .after(crate::scene_view::sync_editor_camera_viewport_target)
                    .after(crate::play::sync_play_camera_target)
                    .after(game_hot::resolve_active_camera_render_profile_system),
            );
    }
}

fn sync_viewport_local_shadow_lights(
    points: Query<&PointLight>,
    spots: Query<&SpotLight>,
    mut flag: ResMut<ViewportLocalShadowLights>,
) {
    flag.0 = has_local_shadow_lights(&points, &spots);
}

fn owner_from_state(mode: EditorMode, possession: PlayPossession) -> ViewportCameraOwner {
    let editing = matches!(mode, EditorMode::Editing);
    let playing_possessed = mode == EditorMode::Playing && possession == PlayPossession::Possessed;
    resolve_viewport_camera_owner(editing, playing_possessed)
}

type CameraFxState = (bool, bool, bool, bool, bool, bool, bool, bool);

fn fx_snapshot(state: CameraFxState) -> ViewportFxSnapshot {
    ViewportFxSnapshot {
        has_stack: state.0,
        has_atmosphere_settings: state.1,
        has_tonemapping: state.2,
        has_bloom: state.3,
        has_ssao: state.4,
        has_taa: state.5,
        has_hdr: state.6,
        has_auto_exposure: state.7,
    }
}

#[derive(Default)]
struct RenderViewCache {
    key: Option<(EditorMode, PlayPossession)>,
    target: Option<AssetId<Image>>,
    profile: Option<ActiveCameraRenderProfile>,
    effective_gi: Option<GiPath>,
    stack: Option<EffectiveRenderStack>,
    local_shadow_lights: Option<bool>,
}

/// Ensures exactly one camera has the full project rendering stack for the current mode.
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn sync_project_render_view(
    mode: Res<State<EditorMode>>,
    possession: Res<PlayPossession>,
    settings: Res<ProjectSettings>,
    profile: Res<ActiveCameraRenderProfile>,
    mut caps: ResMut<RenderingCapabilities>,
    solari_stats: Res<SolariRaytracingSceneStats>,
    viewport_target: Res<ViewportRenderTarget>,
    local_shadow_lights: Res<ViewportLocalShadowLights>,
    mut commands: Commands,
    mut mediums: ResMut<Assets<ScatteringMedium>>,
    editor_cameras: Query<Entity, With<EditorCamera>>,
    player_cameras: Query<Entity, With<PlayerCamera>>,
    stacked: Query<Entity, With<ProjectRenderCamera>>,
    atmospheres: Query<Entity, With<ProjectAtmosphere>>,
    camera_fx: Query<(
        Has<ProjectRenderCamera>,
        Has<AtmosphereSettings>,
        Has<Tonemapping>,
        Has<Bloom>,
        Has<ScreenSpaceAmbientOcclusion>,
        Has<TemporalAntiAliasing>,
        Has<Hdr>,
        Has<AutoExposure>,
    )>,
    mut cache: Local<RenderViewCache>,
) {
    let mode = *mode.get();
    let possession = *possession;
    let owner = owner_from_state(mode, possession);
    let key = (mode, possession);
    let owner_changed = cache.key != Some(key);
    let settings_changed = settings.is_changed();
    let profile_changed = profile.is_changed() || cache.profile != Some(profile.clone());
    cache.profile = Some(profile.clone());
    let stack = game::rendering::effective_viewport_render_stack(
        profile.gi_path,
        &caps,
        Some(&solari_stats),
        local_shadow_lights.0,
    );
    if cache.stack != Some(stack) {
        info!(
            "Rendering stack: requested={:?} effective={:?} fallback={:?} rt_supported={} solari_ready={} local_shadow_lights={} tagged_meshes={} render_instances={} render_directional_lights={} bind_group_ready={} emissive_meshes={}",
            stack.requested_gi_path,
            stack.effective_gi_path,
            stack.fallback_reason,
            caps.rt_supported,
            stack.solari_ready,
            stack.local_shadow_lights,
            solari_stats.tagged_meshes,
            solari_stats.render_instances,
            solari_stats.render_directional_lights,
            solari_stats.render_bind_group_ready,
            solari_stats.emissive_meshes,
        );
        cache.stack = Some(stack);
    }
    let effective_gi = stack.effective_gi_path;
    let solari_readiness_changed = cache.effective_gi != Some(effective_gi);
    cache.effective_gi = Some(effective_gi);
    let local_shadow_lights_changed = cache.local_shadow_lights != Some(local_shadow_lights.0);
    cache.local_shadow_lights = Some(local_shadow_lights.0);

    let active_target = viewport_target.0.as_ref().map(|t| t.image.id());
    let target_changed = cache.target != active_target;
    cache.target = active_target;

    if !owner_changed
        && !settings_changed
        && !target_changed
        && !profile_changed
        && !solari_readiness_changed
        && !local_shadow_lights_changed
    {
        return;
    }
    cache.key = Some(key);

    let Some(_) = active_target else {
        return;
    };

    let active: HashSet<Entity> = match owner {
        ViewportCameraOwner::EditorFly => editor_cameras.iter().collect(),
        ViewportCameraOwner::Player => player_cameras.iter().collect(),
    };

    if owner_changed {
        for entity in stacked.iter() {
            if !active.contains(&entity) {
                clear_viewport_camera_stack(&mut commands, entity);
            }
        }
    }

    if owner_changed
        || settings_changed
        || target_changed
        || profile_changed
        || solari_readiness_changed
        || local_shadow_lights_changed
    {
        sync_project_atmosphere(
            &mut commands,
            &mut mediums,
            profile.atmosphere,
            atmospheres.iter(),
        );
        let force_full_apply = owner_changed || target_changed || solari_readiness_changed;
        for entity in &active {
            let apply = if force_full_apply {
                ViewportStackApply::Full
            } else if let Ok(state) = camera_fx.get(*entity) {
                let snap = fx_snapshot(state);
                if snap.has_stack {
                    ViewportStackApply::Incremental(snap)
                } else {
                    ViewportStackApply::Full
                }
            } else {
                ViewportStackApply::Full
            };
            sync_viewport_camera_stack(
                &mut commands,
                *entity,
                &profile,
                &mut caps,
                Some(&solari_stats),
                local_shadow_lights.0,
                &mut mediums,
                apply,
            );
        }
    }
}
