//! Post-process volume blending and active camera profile resolution.

use bevy::prelude::*;
use settings::{
    resolve_effective_render_stack, resolve_gi_path, write_effective_render_stack,
    ActiveCameraRenderProfile, ActiveVolumeContribution, EffectiveRenderStack, GiPath,
    ProjectSettings, RenderingCapabilities, RenderingSettings, VolumeContribution,
};
use shared::{
    authoring_component_active, AuthoringComponentStates, InspectorOrder, LevelObject,
    PostProcessVolumeDesc, PostProcessVolumeOverrides, COMPONENT_POST_PROCESS_VOLUME,
};

use super::solari::SolariRaytracingSceneStats;
use super::viewport_camera::has_local_shadow_lights;

/// Edge blend weight for a point in volume local space.
pub fn volume_edge_weight(local: Vec3, half_extents: Vec3, blend_distance: f32) -> f32 {
    let dist = half_extents - local.abs();
    if dist.x < 0.0 || dist.y < 0.0 || dist.z < 0.0 {
        return 0.0;
    }
    if blend_distance <= 0.0 {
        return 1.0;
    }
    let min_dist = dist.x.min(dist.y).min(dist.z);
    (min_dist / blend_distance).clamp(0.0, 1.0)
}

/// Applies volume overrides onto project rendering defaults.
pub fn resolve_volume_overrides(
    project: &RenderingSettings,
    desc: &PostProcessVolumeDesc,
) -> ActiveCameraRenderProfile {
    let gi_path = GiPath::Forward;
    let mut profile = ActiveCameraRenderProfile::from_project(project, gi_path);
    apply_overrides(&mut profile, &desc.overrides);
    profile.fullscreen_effect = desc.fullscreen_effect.clone();
    profile
}

fn apply_overrides(
    profile: &mut ActiveCameraRenderProfile,
    overrides: &PostProcessVolumeOverrides,
) {
    if let Some(v) = overrides.exposure_ev100 {
        profile.exposure_ev100 = v;
    }
    if let Some(v) = overrides.fog_color {
        profile.fog_color = [v.r, v.g, v.b];
    }
    if let Some(v) = overrides.fog_density {
        profile.fog_density = v;
    }
    if let Some(v) = overrides.bloom {
        profile.bloom = v;
    }
    if let Some(v) = overrides.taa {
        profile.taa = v;
    }
    if let Some(v) = overrides.ssao {
        profile.ssao = v;
    }
    if let Some(v) = overrides.tonemapping_aces {
        profile.tonemapping_aces = v;
    }
    if let Some(v) = overrides.atmosphere {
        profile.atmosphere = v;
    }
    if let Some(v) = overrides.hdr {
        profile.hdr = v;
    }
}

#[derive(Debug, Clone)]
pub struct VolumeSample {
    entity: Entity,
    weight: f32,
    priority: i32,
    desc: PostProcessVolumeDesc,
}

fn blend_profiles(
    project: &RenderingSettings,
    gi_path: GiPath,
    samples: &[VolumeSample],
) -> ActiveCameraRenderProfile {
    let mut profile = ActiveCameraRenderProfile::from_project(project, gi_path);
    if samples.is_empty() {
        return profile;
    }

    let mut sorted: Vec<&VolumeSample> = samples.iter().filter(|s| s.weight > 0.0).collect();
    sorted.sort_by(|a, b| {
        b.priority
            .cmp(&a.priority)
            .then_with(|| a.entity.index().cmp(&b.entity.index()))
    });

    for sample in &sorted {
        if sample.desc.fullscreen_effect.is_some() {
            profile.fullscreen_effect = sample.desc.fullscreen_effect.clone();
            break;
        }
    }

    for sample in &sorted {
        if sample.desc.overrides.exposure_ev100.is_some() {
            profile.exposure_ev100 = resolve_volume_overrides(project, &sample.desc).exposure_ev100;
            profile.exposure_mode = settings::ExposureMode::Manual;
            break;
        }
    }
    for sample in &sorted {
        if sample.desc.overrides.fog_density.is_some() {
            profile.fog_density = resolve_volume_overrides(project, &sample.desc).fog_density;
            break;
        }
    }
    for sample in &sorted {
        if sample.desc.overrides.fog_color.is_some() {
            profile.fog_color = resolve_volume_overrides(project, &sample.desc).fog_color;
            break;
        }
    }

    // Edge blend for scalars without explicit overrides: weighted by spatial weight.
    let mut total_w = 0.0f32;
    let mut exposure = 0.0f32;
    let mut fog_density = 0.0f32;
    let mut fog_color = [0.0f32; 3];
    let any_exposure_override = sorted
        .iter()
        .any(|s| s.desc.overrides.exposure_ev100.is_some());
    let any_fog_density_override = sorted
        .iter()
        .any(|s| s.desc.overrides.fog_density.is_some());
    let any_fog_color_override = sorted.iter().any(|s| s.desc.overrides.fog_color.is_some());
    for sample in &sorted {
        let resolved = resolve_volume_overrides(project, &sample.desc);
        let w = sample.weight;
        if !any_exposure_override {
            exposure += resolved.exposure_ev100 * w;
        }
        if !any_fog_density_override {
            fog_density += resolved.fog_density * w;
        }
        if !any_fog_color_override {
            fog_color[0] += resolved.fog_color[0] * w;
            fog_color[1] += resolved.fog_color[1] * w;
            fog_color[2] += resolved.fog_color[2] * w;
        }
        total_w += w;
    }
    if total_w > 0.0 {
        if !any_exposure_override {
            profile.exposure_ev100 = exposure / total_w;
        }
        if !any_fog_density_override {
            profile.fog_density = fog_density / total_w;
        }
        if !any_fog_color_override {
            profile.fog_color = fog_color.map(|c| c / total_w);
        }
    }

    for sample in &sorted {
        let o = &sample.desc.overrides;
        if let Some(v) = o.hdr {
            profile.hdr = v;
            break;
        }
    }
    for sample in &sorted {
        if let Some(v) = sample.desc.overrides.bloom {
            profile.bloom = v;
            break;
        }
    }
    for sample in &sorted {
        if let Some(v) = sample.desc.overrides.taa {
            profile.taa = v;
            break;
        }
    }
    for sample in &sorted {
        if let Some(v) = sample.desc.overrides.ssao {
            profile.ssao = v;
            break;
        }
    }
    for sample in &sorted {
        if let Some(v) = sample.desc.overrides.tonemapping_aces {
            profile.tonemapping_aces = v;
            break;
        }
    }
    for sample in &sorted {
        if let Some(v) = sample.desc.overrides.atmosphere {
            profile.atmosphere = v;
            break;
        }
    }

    profile
}

/// Resolves the active camera render profile from project settings and scene volumes.
pub fn resolve_active_camera_render_profile(
    camera_world: Vec3,
    project: &RenderingSettings,
    caps: &RenderingCapabilities,
    volumes: &[(Entity, &PostProcessVolumeDesc, &GlobalTransform)],
) -> (ActiveCameraRenderProfile, Vec<VolumeContribution>) {
    let gi_path = resolve_gi_path(project.gi_mode, caps);
    let mut samples = Vec::new();

    for (entity, desc, global) in volumes {
        let local = global.affine().inverse().transform_point3(camera_world);
        let weight = volume_edge_weight(local, desc.half_extents, desc.blend_distance);
        if weight > 0.0 {
            samples.push(VolumeSample {
                entity: *entity,
                weight,
                priority: desc.priority,
                desc: (*desc).clone(),
            });
        }
    }

    let mut contributions: Vec<VolumeContribution> = samples
        .iter()
        .map(|s| VolumeContribution {
            entity_bits: s.entity.to_bits(),
            weight: s.weight,
            label: s
                .desc
                .label
                .clone()
                .unwrap_or_else(|| format!("Volume {}", s.entity.index())),
            override_count: s.desc.overrides.override_count(),
        })
        .collect();
    contributions.sort_by(|a, b| {
        b.weight
            .partial_cmp(&a.weight)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let profile = blend_profiles(project, gi_path, &samples);
    (profile, contributions)
}

/// Bevy system: updates [`ActiveCameraRenderProfile`] and [`ActiveVolumeContribution`].
#[expect(
    clippy::too_many_arguments,
    reason = "Bevy systems expose independent render resources, queries, and local state"
)]
pub fn sync_active_camera_render_profile(
    mut commands: Commands,
    settings: Res<ProjectSettings>,
    volumes: Query<
        (
            Entity,
            &PostProcessVolumeDesc,
            &GlobalTransform,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ),
        With<LevelObject>,
    >,
    cameras: Query<&GlobalTransform, With<settings::ProjectRenderCamera>>,
    mut profile: ResMut<ActiveCameraRenderProfile>,
    mut contributions: ResMut<ActiveVolumeContribution>,
    mut caps: ResMut<RenderingCapabilities>,
    solari_stats: Res<SolariRaytracingSceneStats>,
    points: Query<&PointLight>,
    spots: Query<&SpotLight>,
    mut last_request: Local<Option<(settings::GiMode, GiPath, bool)>>,
    mut last_stack: Local<Option<EffectiveRenderStack>>,
) {
    let gi_path = resolve_gi_path(settings.rendering.gi_mode, &caps);
    let request_key = (settings.rendering.gi_mode, gi_path, caps.rt_supported);
    if *last_request != Some(request_key) {
        info!(
            "Rendering profile request: gi_mode={:?} requested={:?} rt_supported={}",
            settings.rendering.gi_mode, gi_path, caps.rt_supported
        );
        *last_request = Some(request_key);
    }
    let local_shadow_lights = has_local_shadow_lights(&points, &spots);
    let Some(camera_tf) = cameras.iter().next() else {
        let resolved = ActiveCameraRenderProfile::from_project(&settings.rendering, gi_path);
        *profile = resolved.clone();
        contributions.set_entries(Vec::new());
        publish_effective_stack(
            &mut commands,
            &mut caps,
            &solari_stats,
            resolved.gi_path,
            local_shadow_lights,
            &mut last_stack,
        );
        return;
    };

    let volume_list: Vec<_> = volumes
        .iter()
        .filter(|(_, _, _, states, legacy_order)| {
            authoring_component_active(*states, *legacy_order, COMPONENT_POST_PROCESS_VOLUME)
        })
        .map(|(entity, desc, transform, _, _)| (entity, desc, transform))
        .collect();
    let (resolved, entries) = resolve_active_camera_render_profile(
        camera_tf.translation(),
        &settings.rendering,
        &caps,
        &volume_list,
    );
    *profile = resolved.clone();
    publish_effective_stack(
        &mut commands,
        &mut caps,
        &solari_stats,
        resolved.gi_path,
        local_shadow_lights,
        &mut last_stack,
    );
    contributions.set_entries(entries);
}

fn publish_effective_stack(
    commands: &mut Commands,
    caps: &mut RenderingCapabilities,
    solari_stats: &SolariRaytracingSceneStats,
    requested_gi_path: GiPath,
    local_shadow_lights: bool,
    last_stack: &mut Local<Option<EffectiveRenderStack>>,
) {
    let stack = resolve_effective_render_stack(
        requested_gi_path,
        caps,
        solari_stats.solari_ready(),
        local_shadow_lights,
    );
    write_effective_render_stack(caps, stack);
    commands.insert_resource(stack);

    if **last_stack != Some(stack) {
        info!(
            "Rendering effective 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,
        );
        **last_stack = Some(stack);
    }
}

/// Alias used by [`SolariRenderingPlugin`] and editor ordering.
pub use sync_active_camera_render_profile as resolve_active_camera_render_profile_system;

/// Returns entities whose post-process volumes contain `world_pos` with weight > 0.
pub fn sample_volumes_at(
    world_pos: Vec3,
    volumes: &[(Entity, &PostProcessVolumeDesc, &GlobalTransform)],
) -> Vec<Entity> {
    volumes
        .iter()
        .filter_map(|(entity, desc, global)| {
            let local = global.affine().inverse().transform_point3(world_pos);
            let weight = volume_edge_weight(local, desc.half_extents, desc.blend_distance);
            (weight > 0.0).then_some(*entity)
        })
        .collect()
}

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

    fn default_project() -> RenderingSettings {
        RenderingSettings::default()
    }

    #[test]
    fn edge_weight_center_is_one() {
        let w = volume_edge_weight(Vec3::ZERO, Vec3::ONE, 0.5);
        assert!((w - 1.0).abs() < 1e-5);
    }

    #[test]
    fn edge_weight_outside_is_zero() {
        let w = volume_edge_weight(Vec3::new(2.0, 0.0, 0.0), Vec3::ONE, 0.5);
        assert_eq!(w, 0.0);
    }

    #[test]
    fn inherit_vs_override_exposure() {
        let project = default_project();
        let mut desc = PostProcessVolumeDesc::default();
        desc.overrides.exposure_ev100 = Some(8.0);
        let resolved = resolve_volume_overrides(&project, &desc);
        assert!((resolved.exposure_ev100 - 8.0).abs() < 1e-5);
        assert_eq!(resolved.bloom, project.bloom);
    }

    #[test]
    fn priority_wins_at_overlap_center() {
        let project = default_project();
        let low = VolumeSample {
            entity: Entity::from_raw_u32(1).unwrap(),
            weight: 1.0,
            priority: 0,
            desc: PostProcessVolumeDesc {
                overrides: PostProcessVolumeOverrides {
                    exposure_ev100: Some(5.0),
                    ..Default::default()
                },
                ..Default::default()
            },
        };
        let high = VolumeSample {
            entity: Entity::from_raw_u32(2).unwrap(),
            weight: 1.0,
            priority: 10,
            desc: PostProcessVolumeDesc {
                overrides: PostProcessVolumeOverrides {
                    exposure_ev100: Some(15.0),
                    ..Default::default()
                },
                ..Default::default()
            },
        };
        let profile = blend_profiles(&project, GiPath::Forward, &[low, high]);
        assert!((profile.exposure_ev100 - 15.0).abs() < 1e-4);
    }

    #[test]
    fn volume_exposure_override_forces_manual_mode() {
        let mut project = default_project();
        project.exposure_mode = settings::ExposureMode::Auto;
        let sample = VolumeSample {
            entity: Entity::from_raw_u32(1).unwrap(),
            weight: 1.0,
            priority: 0,
            desc: PostProcessVolumeDesc {
                overrides: PostProcessVolumeOverrides {
                    exposure_ev100: Some(9.0),
                    ..Default::default()
                },
                ..Default::default()
            },
        };
        let profile = blend_profiles(&project, GiPath::Forward, &[sample]);
        assert_eq!(profile.exposure_mode, settings::ExposureMode::Manual);
        assert!((profile.exposure_ev100 - 9.0).abs() < 1e-5);
    }

    #[test]
    fn solari_stays_active_when_rt_is_supported() {
        let mut project = default_project();
        project.gi_mode = settings::GiMode::Solari;
        let caps = RenderingCapabilities {
            rt_supported: true,
            ..Default::default()
        };

        assert_eq!(
            resolve_gi_path(project.gi_mode, &caps),
            GiPath::SolariDeferred
        );
    }

    #[test]
    fn fog_color_override() {
        let project = default_project();
        let mut desc = PostProcessVolumeDesc::default();
        desc.overrides.fog_color = Some(ColorDesc::srgb(0.1, 0.2, 0.3));
        let resolved = resolve_volume_overrides(&project, &desc);
        assert!((resolved.fog_color[0] - 0.1).abs() < 1e-5);
    }
}
