//! Generates runtime render/physics/light components from reflectable authoring data.

mod brushes;
mod lights;
mod materials;
mod models;
mod physics;
mod prefabs;
mod primitives;
mod skinned_meshes;
mod static_meshes;
pub mod strip;
mod visibility;

pub use lights::cascade_config_from_rendering;
pub use strip::{strip_hydrated, strip_hydrated_entity};

use bevy::ecs::system::SystemState;
use bevy::prelude::*;

use brushes::{hydrate_brushes, spawn_brush_mesh, HydratedBrushMesh};
use lights::{hydrate_lights, reconcile_missing_runtime_lights};
use materials::hydrate_materials;
pub use materials::material_from_desc;
use models::hydrate_models;
pub use models::HydratedModelRoot;
use physics::hydrate_physics;
use prefabs::{hydrate_prefabs, tag_hydrated_prefab_members};
pub use prefabs::{HydratedPrefabMember, HydratedPrefabReady, PrefabHydrationBlocked};
use primitives::{hydrate_primitives, primitive_mesh};
pub use skinned_meshes::HydratedSkinnedMeshRoot;
use skinned_meshes::{
    apply_skinned_renderer_material_changes, cleanup_removed_skinned_mesh_renderers,
    hydrate_skinned_mesh_renderers, mark_skinned_materials_ready,
};
use static_meshes::{
    hydrate_static_mesh_renderers, spawn_static_mesh_parts, HydratedStaticMeshPart,
    StaticMeshArtifactCache,
};
use visibility::{
    ensure_level_object_visibility_hierarchy, init_editor_visibility_on_spawn,
    sync_editor_visibility, visibility_from_editor,
};

use crate::{
    authoring_component_active, AuthoringComponentStates, BrushDesc, ColliderDesc,
    EditorVisibility, InspectorOrder, LevelObject, MaterialDesc, MaterialOverride, Primitive,
    StaticMeshRenderer, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC,
    COMPONENT_MATERIAL_DESC, COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER,
};

/// Registers hydration systems in deterministic order.
pub struct HydrationPlugin;

#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum HydrationSet {
    Visibility,
    Content,
}

impl Plugin for HydrationPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<StaticMeshArtifactCache>()
            .add_observer(tag_hydrated_prefab_members)
            .add_observer(mark_skinned_materials_ready)
            .add_observer(crate::prefab_overrides::apply_prefab_overrides_on_ready)
            .configure_sets(
                Update,
                (HydrationSet::Visibility, HydrationSet::Content).chain(),
            )
            .add_systems(
                Update,
                (
                    hydrate_primitives,
                    hydrate_brushes,
                    hydrate_materials,
                    hydrate_lights,
                    reconcile_missing_runtime_lights,
                    hydrate_static_mesh_renderers,
                    hydrate_skinned_mesh_renderers,
                    apply_skinned_renderer_material_changes,
                    cleanup_removed_skinned_mesh_renderers,
                    hydrate_models,
                    hydrate_prefabs,
                    hydrate_physics,
                )
                    .chain()
                    .in_set(HydrationSet::Content),
            )
            .add_systems(
                Update,
                (
                    ensure_level_object_visibility_hierarchy,
                    sync_editor_visibility,
                    init_editor_visibility_on_spawn,
                )
                    .chain()
                    .in_set(HydrationSet::Visibility),
            );
    }
}

/// Runs the full hydration chain immediately (e.g. after scene load in the editor).
#[expect(
    clippy::type_complexity,
    reason = "exclusive hydration snapshots several authored component combinations"
)]
pub fn flush_level_object_hydration(world: &mut World) {
    if !world.contains_resource::<StaticMeshArtifactCache>() {
        world.insert_resource(StaticMeshArtifactCache::default());
    }

    let primitives: Vec<(Entity, Primitive)> = world
        .query_filtered::<(
            Entity,
            &Primitive,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .filter(|(_, _, states, order)| {
            authoring_component_active(*states, *order, COMPONENT_PRIMITIVE)
        })
        .map(|(entity, primitive, _, _)| (entity, primitive.clone()))
        .collect();

    let material_targets: Vec<(Entity, MaterialDesc)> = world
        .query_filtered::<(
            Entity,
            &MaterialDesc,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .filter(|(_, _, states, order)| {
            authoring_component_active(*states, *order, COMPONENT_MATERIAL_DESC)
        })
        .map(|(entity, desc, _, _)| (entity, desc.clone()))
        .collect();

    let brush_entities_for_cleanup: Vec<Entity> = world
        .query_filtered::<(Entity, &BrushDesc), With<LevelObject>>()
        .iter(world)
        .map(|(entity, _)| entity)
        .collect();

    let brush_targets: Vec<(
        Entity,
        BrushDesc,
        Option<MaterialDesc>,
        Option<ColliderDesc>,
    )> = world
        .query_filtered::<(
            Entity,
            &BrushDesc,
            Option<&MaterialDesc>,
            Option<&ColliderDesc>,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .filter(|(_, _, _, _, states, order)| {
            authoring_component_active(*states, *order, COMPONENT_BRUSH_DESC)
        })
        .map(|(entity, brush, material, collider, states, order)| {
            (
                entity,
                brush.clone(),
                material
                    .filter(|_| authoring_component_active(states, order, COMPONENT_MATERIAL_DESC))
                    .cloned(),
                collider
                    .filter(|_| authoring_component_active(states, order, COMPONENT_COLLIDER_DESC))
                    .cloned(),
            )
        })
        .collect();

    let static_mesh_entities_for_cleanup: Vec<Entity> = world
        .query_filtered::<(Entity, &StaticMeshRenderer), With<LevelObject>>()
        .iter(world)
        .map(|(entity, _)| entity)
        .collect();

    let static_mesh_renderers: Vec<(
        Entity,
        StaticMeshRenderer,
        Option<MaterialDesc>,
        Option<MaterialOverride>,
        Option<ColliderDesc>,
    )> = world
        .query_filtered::<(
            Entity,
            &StaticMeshRenderer,
            Option<&MaterialDesc>,
            Option<&MaterialOverride>,
            Option<&ColliderDesc>,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .filter(|(_, _, _, _, _, states, order)| {
            authoring_component_active(*states, *order, COMPONENT_STATIC_MESH_RENDERER)
        })
        .map(
            |(entity, renderer, material, material_override, collider, states, order)| {
                (
                    entity,
                    renderer.clone(),
                    material
                        .filter(|_| {
                            authoring_component_active(states, order, COMPONENT_MATERIAL_DESC)
                        })
                        .cloned(),
                    material_override.cloned(),
                    collider
                        .filter(|_| {
                            authoring_component_active(states, order, COMPONENT_COLLIDER_DESC)
                        })
                        .cloned(),
                )
            },
        )
        .collect();

    let mut generated_static_mesh_parts = Vec::new();
    for entity in &static_mesh_entities_for_cleanup {
        if let Some(children) = world.get::<Children>(*entity) {
            for child in children.iter() {
                if world.get::<HydratedStaticMeshPart>(child).is_some() {
                    generated_static_mesh_parts.push(child);
                }
            }
        }
    }

    let mut generated_brush_meshes = Vec::new();
    for entity in &brush_entities_for_cleanup {
        if let Some(children) = world.get::<Children>(*entity) {
            for child in children.iter() {
                if world.get::<HydratedBrushMesh>(child).is_some() {
                    generated_brush_meshes.push(child);
                }
            }
        }
    }

    let visibility_targets: Vec<(Entity, EditorVisibility)> = world
        .query_filtered::<(Entity, &EditorVisibility), With<LevelObject>>()
        .iter(world)
        .map(|(entity, vis)| (entity, *vis))
        .collect();

    let rendering = world
        .get_resource::<settings::ProjectSettings>()
        .map(|s| s.rendering.clone())
        .unwrap_or_default();
    let profile = world
        .get_resource::<settings::ActiveCameraRenderProfile>()
        .cloned();
    let caps = world
        .get_resource::<settings::RenderingCapabilities>()
        .cloned();

    let lights: Vec<(Entity, crate::LightDesc)> = world
        .query_filtered::<(
            Entity,
            &crate::LightDesc,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .filter(|(_, light, states, order)| {
            authoring_component_active(*states, *order, COMPONENT_LIGHT_DESC)
                && !lights::local_light_disabled_in_solari(light, profile.as_ref(), caps.as_ref())
        })
        .map(|(entity, light, _, _)| (entity, light.clone()))
        .collect();

    let mut state: SystemState<(
        Commands,
        Res<AssetServer>,
        ResMut<Assets<Mesh>>,
        ResMut<Assets<StandardMaterial>>,
        ResMut<StaticMeshArtifactCache>,
    )> = SystemState::new(world);

    {
        let (mut commands, asset_server, mut meshes, mut materials, mut artifact_cache) = state
            .get_mut(world)
            .expect("hydrate_level_objects system params should be valid");

        for (entity, primitive) in primitives {
            let mesh = meshes.add(primitive_mesh(&primitive));
            commands.entity(entity).insert(Mesh3d(mesh));
        }

        for (entity, desc) in material_targets {
            let material = materials.add(material_from_desc(&asset_server, &desc));
            commands.entity(entity).insert(MeshMaterial3d(material));
        }

        for child in generated_brush_meshes {
            commands.entity(child).despawn();
        }

        for (entity, brush, material, collider) in &brush_targets {
            spawn_brush_mesh(
                &mut commands,
                &asset_server,
                &mut meshes,
                &mut materials,
                *entity,
                brush,
                material.as_ref(),
                collider.as_ref(),
            );
        }

        for child in generated_static_mesh_parts {
            commands.entity(child).despawn();
        }

        for (entity, renderer, material, material_override, collider) in &static_mesh_renderers {
            spawn_static_mesh_parts(
                &mut commands,
                &asset_server,
                &mut materials,
                &mut artifact_cache,
                *entity,
                renderer,
                material.as_ref(),
                material_override.as_ref(),
                collider.as_ref(),
            );
        }

        for (entity, light) in lights {
            let mut entity_commands = commands.entity(entity);
            lights::strip_light_components(&mut entity_commands);
            lights::insert_light_from_desc(&mut entity_commands, &light, &rendering);
        }
    }

    state.apply(world);

    for (entity, editor) in visibility_targets {
        let visibility = visibility_from_editor(editor);
        if let Some(mut vis) = world.get_mut::<Visibility>(entity) {
            *vis = visibility;
        } else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
            entity_mut.insert(visibility);
        }
        if world.get::<InheritedVisibility>(entity).is_none() {
            if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
                entity_mut.insert(InheritedVisibility::default());
            }
        }
        if world.get::<ViewVisibility>(entity).is_none() {
            if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
                entity_mut.insert(ViewVisibility::default());
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::flush_level_object_hydration;
    use super::strip::strip_hydrated_entity;
    use crate::{
        AuthoringLightKind, ColorDesc, EditorVisibility, InspectorOrder, LevelObject, LightDesc,
        MaterialDesc, Primitive, COMPONENT_LIGHT_DESC,
    };
    use avian3d::prelude::*;
    use bevy::prelude::*;

    #[test]
    fn flush_hydration_spawns_runtime_light_from_light_desc() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();

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

        flush_level_object_hydration(app.world_mut());

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

    #[test]
    fn flush_hydration_initializes_level_object_visibility_hierarchy() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();

        let entity = app
            .world_mut()
            .spawn((LevelObject, EditorVisibility { visible: false }))
            .id();

        flush_level_object_hydration(app.world_mut());

        assert_eq!(
            app.world().get::<Visibility>(entity),
            Some(&Visibility::Hidden)
        );
        assert!(app.world().get::<InheritedVisibility>(entity).is_some());
        assert!(app.world().get::<ViewVisibility>(entity).is_some());
    }

    #[test]
    fn flush_hydration_skips_point_light_when_solari_is_active() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();
        app.world_mut()
            .insert_resource(settings::ActiveCameraRenderProfile {
                gi_path: settings::GiPath::SolariDeferred,
                ..Default::default()
            });
        app.world_mut()
            .insert_resource(settings::RenderingCapabilities {
                rt_supported: true,
                ..Default::default()
            });

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

        flush_level_object_hydration(app.world_mut());

        assert!(app.world().get::<PointLight>(entity).is_none());
    }

    #[test]
    fn flush_hydration_keeps_directional_light_when_solari_is_active() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();
        app.world_mut()
            .insert_resource(settings::ActiveCameraRenderProfile {
                gi_path: settings::GiPath::SolariDeferred,
                ..Default::default()
            });
        app.world_mut()
            .insert_resource(settings::RenderingCapabilities {
                rt_supported: true,
                ..Default::default()
            });

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

        flush_level_object_hydration(app.world_mut());

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

    #[test]
    fn flush_hydration_skips_inactive_light_desc() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();

        let mut order = InspectorOrder::default();
        order.set_component_active(COMPONENT_LIGHT_DESC, false);
        let entity = app
            .world_mut()
            .spawn((
                LevelObject,
                LightDesc::for_kind(AuthoringLightKind::Point),
                order,
            ))
            .id();

        flush_level_object_hydration(app.world_mut());

        assert!(app.world().get::<PointLight>(entity).is_none());
    }

    #[test]
    fn strip_hydrated_removes_runtime_mesh_and_lights() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);

        let entity = app
            .world_mut()
            .spawn((
                Mesh3d::default(),
                MeshMaterial3d::<StandardMaterial>::default(),
                PointLight::default(),
                RigidBody::Static,
                Collider::sphere(1.0),
            ))
            .id();

        {
            let mut entity_mut = app.world_mut().entity_mut(entity);
            strip_hydrated_entity(&mut entity_mut);
        }

        let world = app.world();
        assert!(world.get::<Mesh3d>(entity).is_none());
        assert!(world.get::<PointLight>(entity).is_none());
        assert!(world.get::<RigidBody>(entity).is_none());
    }

    #[test]
    fn flush_hydration_spawns_mesh_material_from_primitive_and_desc() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        app.add_plugins(AssetPlugin::default());
        app.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default());
        app.init_asset::<Mesh>();

        let material = MaterialDesc::new(ColorDesc::srgb(0.85, 0.85, 0.88), 0.0, 0.3);
        let entity = app
            .world_mut()
            .spawn((
                LevelObject,
                Primitive::cuboid(Vec3::new(1.2, 7.0, 1.2)),
                material,
            ))
            .id();

        flush_level_object_hydration(app.world_mut());

        assert!(app.world().get::<Mesh3d>(entity).is_some());
        assert!(app
            .world()
            .get::<MeshMaterial3d<StandardMaterial>>(entity)
            .is_some());
    }
}
