//! Bevy lights from [`LightDesc`] authoring data.

use bevy::light::{CascadeShadowConfig, CascadeShadowConfigBuilder};
use bevy::prelude::*;
use settings::{ActiveCameraRenderProfile, GiPath, RenderingCapabilities, RenderingSettings};

use crate::{
    authoring_component_active, AuthoringComponentStates, AuthoringLightKind, InspectorOrder,
    LevelObject, LightDesc, COMPONENT_LIGHT_DESC,
};

/// Legacy directional lux below this is treated as invalid (see ADR 0011).
pub const MIN_DIRECTIONAL_LUX: f32 = 1_000.0;
/// Documented outdoor default matching [`RenderingSettings`].
pub const DEFAULT_DIRECTIONAL_LUX: f32 = 100_000.0;

/// Builds cascaded shadow config from project rendering shadow fields (matches [`game_hot::spawn_sun`]).
pub fn cascade_config_from_rendering(rendering: &RenderingSettings) -> CascadeShadowConfig {
    CascadeShadowConfigBuilder {
        num_cascades: rendering.shadow_cascades as usize,
        first_cascade_far_bound: rendering.shadow_first_cascade,
        maximum_distance: rendering.shadow_max_distance,
        ..default()
    }
    .build()
}

#[allow(clippy::type_complexity)]
pub fn hydrate_lights(
    mut commands: Commands,
    settings: Res<settings::ProjectSettings>,
    profile: Option<Res<ActiveCameraRenderProfile>>,
    caps: Option<Res<RenderingCapabilities>>,
    lights: Query<
        (
            Entity,
            Ref<LightDesc>,
            Option<Ref<AuthoringComponentStates>>,
            Option<Ref<InspectorOrder>>,
        ),
        With<LevelObject>,
    >,
) {
    let rendering = &settings.rendering;
    let render_stack_changed = profile.as_ref().is_some_and(|profile| profile.is_changed())
        || caps.as_ref().is_some_and(|caps| caps.is_changed());
    let profile = profile.as_deref();
    let caps = caps.as_deref();
    for (entity, light, states, legacy_order) in &lights {
        let state_changed = states.as_ref().is_some_and(|states| states.is_changed());
        let legacy_changed = legacy_order
            .as_ref()
            .is_some_and(|order| order.is_changed());
        if !light.is_changed() && !state_changed && !legacy_changed && !render_stack_changed {
            continue;
        }
        let mut entity_commands = commands.entity(entity);
        strip_light_components(&mut entity_commands);
        if !authoring_component_active(
            states.as_deref(),
            legacy_order.as_deref(),
            COMPONENT_LIGHT_DESC,
        ) {
            continue;
        }
        if local_light_disabled_in_solari(&light, profile, caps) {
            continue;
        }
        insert_light_from_desc(&mut entity_commands, &light, rendering);
    }
}

pub(crate) fn strip_light_components(entity: &mut EntityCommands<'_>) {
    entity
        .remove::<PointLight>()
        .remove::<SpotLight>()
        .remove::<DirectionalLight>()
        .remove::<CascadeShadowConfig>();
}

pub(crate) fn normalized_directional_lux(intensity: f32) -> f32 {
    if intensity < MIN_DIRECTIONAL_LUX {
        warn!(
            "Directional LightDesc intensity {intensity:.1} lux is below {MIN_DIRECTIONAL_LUX}; \
             using {DEFAULT_DIRECTIONAL_LUX} lux"
        );
        DEFAULT_DIRECTIONAL_LUX
    } else {
        intensity
    }
}

/// Re-applies runtime lights when authoring [`LightDesc`] exists but hydration was stripped.
#[allow(clippy::type_complexity)]
#[expect(
    clippy::too_many_arguments,
    reason = "Bevy systems expose independent resource and query parameters"
)]
pub fn reconcile_missing_runtime_lights(
    mut commands: Commands,
    settings: Res<settings::ProjectSettings>,
    profile: Option<Res<ActiveCameraRenderProfile>>,
    caps: Option<Res<RenderingCapabilities>>,
    lights: Query<
        (
            Entity,
            &LightDesc,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ),
        With<LevelObject>,
    >,
    points: Query<(), With<PointLight>>,
    spots: Query<(), With<SpotLight>>,
    directionals: Query<(), With<DirectionalLight>>,
) {
    let rendering = &settings.rendering;
    let profile = profile.as_deref();
    let caps = caps.as_deref();
    for (entity, light, states, legacy_order) in &lights {
        if !authoring_component_active(states, legacy_order, COMPONENT_LIGHT_DESC) {
            continue;
        }
        if local_light_disabled_in_solari(light, profile, caps) {
            if points.contains(entity) || spots.contains(entity) || directionals.contains(entity) {
                let mut entity_commands = commands.entity(entity);
                strip_light_components(&mut entity_commands);
            }
            continue;
        }
        let missing = match light.kind {
            AuthoringLightKind::Point => !points.contains(entity),
            AuthoringLightKind::Spot => !spots.contains(entity),
            AuthoringLightKind::Directional => !directionals.contains(entity),
        };
        if !missing {
            continue;
        }
        let mut entity_commands = commands.entity(entity);
        strip_light_components(&mut entity_commands);
        insert_light_from_desc(&mut entity_commands, light, rendering);
    }
}

pub(crate) fn local_light_disabled_in_solari(
    light: &LightDesc,
    profile: Option<&ActiveCameraRenderProfile>,
    caps: Option<&RenderingCapabilities>,
) -> bool {
    matches!(
        light.kind,
        AuthoringLightKind::Point | AuthoringLightKind::Spot
    ) && profile.is_some_and(|profile| profile.gi_path == GiPath::SolariDeferred)
        && caps.is_some_and(|caps| caps.rt_supported)
}

pub(crate) fn insert_light_from_desc(
    entity: &mut EntityCommands<'_>,
    light: &LightDesc,
    rendering: &RenderingSettings,
) {
    let color = light.color.to_color();
    match light.kind {
        AuthoringLightKind::Point => {
            entity.insert((
                PointLight {
                    color,
                    intensity: light.intensity,
                    range: light.range,
                    shadow_maps_enabled: light.shadows,
                    ..default()
                },
                Visibility::Visible,
            ));
        }
        AuthoringLightKind::Spot => {
            entity.insert((
                SpotLight {
                    color,
                    intensity: light.intensity,
                    range: light.range,
                    shadow_maps_enabled: light.shadows,
                    outer_angle: light.outer_angle_deg.to_radians(),
                    inner_angle: light.inner_angle_deg.to_radians(),
                    ..default()
                },
                Visibility::Visible,
            ));
        }
        AuthoringLightKind::Directional => {
            entity.insert((
                DirectionalLight {
                    color,
                    illuminance: normalized_directional_lux(light.intensity),
                    shadow_maps_enabled: light.shadows,
                    ..default()
                },
                Visibility::Visible,
            ));
            if light.shadows {
                entity.insert(cascade_config_from_rendering(rendering));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{AuthoringLightKind, ColorDesc, LevelObject, LightDesc};
    use bevy::ecs::world::CommandQueue;
    use settings::{ActiveCameraRenderProfile, GiPath, RenderingCapabilities, RenderingSettings};

    #[test]
    fn light_kind_swap_replaces_runtime_types() {
        let mut world = World::new();
        let mut queue = CommandQueue::default();
        let entity = world.spawn(LevelObject).id();
        let rendering = RenderingSettings::default();
        let spot = LightDesc::for_kind(AuthoringLightKind::Spot);
        {
            let mut commands = Commands::new(&mut queue, &world);
            let mut entity_commands = commands.entity(entity);
            insert_light_from_desc(&mut entity_commands, &spot, &rendering);
        }
        queue.apply(&mut world);
        assert!(world.get::<SpotLight>(entity).is_some());

        let point = LightDesc::for_kind(AuthoringLightKind::Point);
        queue = CommandQueue::default();
        {
            let mut commands = Commands::new(&mut queue, &world);
            let mut entity_commands = commands.entity(entity);
            strip_light_components(&mut entity_commands);
            insert_light_from_desc(&mut entity_commands, &point, &rendering);
        }
        queue.apply(&mut world);
        assert!(world.get::<PointLight>(entity).is_some());
        assert!(world.get::<SpotLight>(entity).is_none());
    }

    #[test]
    fn directional_shadows_insert_cascade_config() {
        let mut world = World::new();
        let mut queue = CommandQueue::default();
        let entity = world.spawn(LevelObject).id();
        let rendering = RenderingSettings::default();
        let mut light = LightDesc::for_kind(AuthoringLightKind::Directional);
        light.shadows = true;
        {
            let mut commands = Commands::new(&mut queue, &world);
            let mut entity_commands = commands.entity(entity);
            insert_light_from_desc(&mut entity_commands, &light, &rendering);
        }
        queue.apply(&mut world);
        assert!(world.get::<DirectionalLight>(entity).is_some());
        let cascades = world.get::<CascadeShadowConfig>(entity).unwrap();
        let expected = cascade_config_from_rendering(&rendering);
        assert_eq!(cascades.bounds.len(), expected.bounds.len());
        assert_eq!(
            cascades.bounds.last().copied(),
            expected.bounds.last().copied()
        );
        assert!(world.get::<Visibility>(entity).is_some());
    }

    #[test]
    fn scene_directional_cascades_match_project_shadow_settings() {
        let rendering = RenderingSettings {
            shadow_cascades: 3,
            shadow_first_cascade: 12.0,
            shadow_max_distance: 400.0,
            ..Default::default()
        };
        let config = cascade_config_from_rendering(&rendering);
        let expected = cascade_config_from_rendering(&rendering);
        assert_eq!(config.bounds, expected.bounds);
        assert_eq!(config.bounds.len(), 3);
    }

    #[test]
    fn hydrated_point_and_spot_lights_insert_visibility() {
        let mut world = World::new();
        let mut queue = CommandQueue::default();
        let point_entity = world.spawn(LevelObject).id();
        let spot_entity = world.spawn(LevelObject).id();
        let rendering = RenderingSettings::default();
        {
            let mut commands = Commands::new(&mut queue, &world);
            insert_light_from_desc(
                &mut commands.entity(point_entity),
                &LightDesc::for_kind(AuthoringLightKind::Point),
                &rendering,
            );
            insert_light_from_desc(
                &mut commands.entity(spot_entity),
                &LightDesc::for_kind(AuthoringLightKind::Spot),
                &rendering,
            );
        }
        queue.apply(&mut world);

        assert!(world.get::<PointLight>(point_entity).is_some());
        assert!(world.get::<Visibility>(point_entity).is_some());
        assert!(world.get::<SpotLight>(spot_entity).is_some());
        assert!(world.get::<Visibility>(spot_entity).is_some());
    }

    #[test]
    fn solari_disables_point_and_spot_light_hydration() {
        let profile = ActiveCameraRenderProfile {
            gi_path: GiPath::SolariDeferred,
            ..Default::default()
        };
        let caps = RenderingCapabilities {
            rt_supported: true,
            ..Default::default()
        };

        assert!(local_light_disabled_in_solari(
            &LightDesc::for_kind(AuthoringLightKind::Point),
            Some(&profile),
            Some(&caps)
        ));
        assert!(local_light_disabled_in_solari(
            &LightDesc::for_kind(AuthoringLightKind::Spot),
            Some(&profile),
            Some(&caps)
        ));
        assert!(!local_light_disabled_in_solari(
            &LightDesc::for_kind(AuthoringLightKind::Directional),
            Some(&profile),
            Some(&caps)
        ));
    }

    #[test]
    fn point_light_default_casts_shadows() {
        let light = LightDesc::for_kind(AuthoringLightKind::Point);
        assert!(light.shadows);
    }

    #[test]
    fn spot_angles_use_degrees_converted_to_radians() {
        let light = LightDesc {
            kind: AuthoringLightKind::Spot,
            inner_angle_deg: 20.0,
            outer_angle_deg: 35.0,
            ..LightDesc::for_kind(AuthoringLightKind::Spot)
        };
        assert!((light.inner_angle_deg.to_radians() - 20.0_f32.to_radians()).abs() < f32::EPSILON);
        let _ = ColorDesc::srgb(1.0, 1.0, 1.0);
    }

    #[test]
    fn low_directional_lux_is_clamped_at_hydration() {
        assert_eq!(normalized_directional_lux(50.0), DEFAULT_DIRECTIONAL_LUX);
        assert_eq!(normalized_directional_lux(100_000.0), 100_000.0);
    }

    #[test]
    fn reconcile_missing_runtime_lights_restores_directional() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.insert_resource(settings::ProjectSettings::default());
        app.add_systems(Update, reconcile_missing_runtime_lights);

        let entity = app
            .world_mut()
            .spawn((
                LevelObject,
                LightDesc::for_kind(AuthoringLightKind::Directional),
            ))
            .id();

        app.update();

        assert!(app.world().get::<DirectionalLight>(entity).is_some());
    }
}
