//! Shared editor/game authoring data and hydration.
//!
//! Saved levels are Bevy dynamic scenes made from these small, reflectable
//! authoring components. Runtime-only render/physics components are generated
//! from them by hydration systems in both the game and editor.

mod actor;
mod animation;
pub mod brush_math;
mod components;
mod hydration;
mod material_asset;
mod navigation;
mod post_process_effect_asset;
mod prefab_overrides;
mod renderer_material;
mod rendering_profile_asset;

pub use actor::{infer_actor_kind, validate_actor, ActorValidationError};
pub use animation::*;
pub use components::*;
pub use hydration::{
    cascade_config_from_rendering, flush_level_object_hydration, material_from_desc,
    strip_hydrated, strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember,
    HydratedPrefabReady, HydratedSkinnedMeshRoot, HydrationPlugin, PrefabHydrationBlocked,
};
pub use material_asset::{
    load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset,
    MaterialRenderState, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset,
    MATERIAL_ASSET_SCHEMA_VERSION, MATERIAL_INSTANCE_SCHEMA_VERSION, SURFACE_SHADER_SCHEMA_VERSION,
};
pub use navigation::*;
pub use post_process_effect_asset::{PostProcessEffectAsset, PostProcessEffectKind};
pub use prefab_overrides::{
    apply_prefab_overrides_on_ready, apply_serialized_navigation_property_override,
    decode_prefab_overrides, encode_prefab_overrides, PrefabActorPath, PrefabComponentOverride,
    PrefabOverrides, PrefabPropertyOverride, PrefabStructuralOverride,
    PREFAB_OVERRIDE_FORMAT_VERSION,
};
pub use renderer_material::*;
pub use rendering_profile_asset::RenderingProfileAsset;

/// Converts catalog-style paths (`assets/models/foo.fbx`) to Bevy AssetServer paths.
pub fn asset_server_path(catalog_path: &str) -> String {
    let normalized = catalog_path.replace('\\', "/");
    normalized
        .strip_prefix("assets/")
        .map(str::to_string)
        .unwrap_or(normalized)
}

/// Returns the typed [`StandardMaterial`] asset path for a source material label.
///
/// Bevy's glTF loader owns `#MaterialN` as a `GltfMaterial`; the PBR extension
/// publishes its renderable conversion under the nested `#MaterialN/std` label.
/// Other model loaders, including `bevy_ufbx`, expose `StandardMaterial` at the
/// original label.
pub fn standard_material_asset_path(asset_path: &str, material_label: &str) -> String {
    let lower_path = asset_path.to_ascii_lowercase();
    let gltf_source = lower_path.ends_with(".gltf") || lower_path.ends_with(".glb");
    let label = if gltf_source && !material_label.ends_with("/std") {
        format!("{material_label}/std")
    } else {
        material_label.to_string()
    };
    format!("{asset_path}#{label}")
}

use bevy::prelude::*;

/// Registers all reflectable authoring types and installs hydration systems.
pub struct SharedTypesPlugin;

impl Plugin for SharedTypesPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<ActorKind>()
            .register_type::<ActorId>()
            .register_type::<ActorName>()
            .register_type::<SceneComposition>()
            .register_type::<SubsceneReference>()
            .register_type::<HierarchySiblingIndex>()
            .register_type::<InspectorOrder>()
            .register_type::<AuthoringComponentStates>()
            .register_type::<InspectorComponentState>()
            .register_type::<ComponentInstanceId>()
            .register_type::<EditorAssetRef>()
            .register_type::<MaterialRef>()
            .register_type::<RendererMaterialSlot>()
            .register_type::<RendererMaterialSet>()
            .register_type::<OrphanedMaterialAssignment>()
            .register_type::<AnimationSkeletonSignature>()
            .register_type::<SkinnedMeshRenderer>()
            .register_type::<AnimationPlaybackRange>()
            .register_type::<AnimationEventDesc>()
            .register_type::<AnimationStateDesc>()
            .register_type::<AnimationControllerDesc>()
            .register_type::<NavigationAgentProfile>()
            .register_type::<NavigationPathSample>()
            .register_type::<NavigationBounds>()
            .register_type::<NavigationObstacle>()
            .register_type::<NavigationArea>()
            .register_type::<NavigationLink>()
            .register_type::<AudioSourceDesc>()
            .register_type::<AudioListenerDesc>()
            .register_type::<AudioAttenuationDesc>()
            .register_type::<AudioRolloff>()
            .register_type::<EditorVisibility>()
            .register_type::<LevelObject>()
            .register_type::<RaytracingExcluded>()
            .register_type::<PlayerSpawn>()
            .register_type::<ProjectSun>()
            .register_type::<Primitive>()
            .register_type::<PrimitiveShape>()
            .register_type::<BrushDesc>()
            .register_type::<BrushKind>()
            .register_type::<BrushPlaneDesc>()
            .register_type::<BrushFaceDesc>()
            .register_type::<StaticMeshRenderer>()
            .register_type::<StaticMeshRendererEntry>()
            .register_type::<StaticMeshAssetRef>()
            .register_type::<StaticMeshMaterialSlot>()
            .register_type::<StaticMeshColliderMode>()
            .register_type::<MaterialDesc>()
            .register_type::<MaterialShaderKind>()
            .register_type::<ShaderRefDesc>()
            .register_type::<MaterialParameterValue>()
            .register_type::<MaterialParameter>()
            .register_type::<MaterialTextureBinding>()
            .register_type::<MaterialSlotOverride>()
            .register_type::<MaterialOverride>()
            .register_type::<ColorDesc>()
            .register_type::<LightDesc>()
            .register_type::<AuthoringLightKind>()
            .register_type::<RigidBodyDesc>()
            .register_type::<ColliderDesc>()
            .register_type::<ColliderShapeDesc>()
            .register_type::<MeshColliderCooking>()
            .register_type::<PhysicsBody>()
            .register_type::<AuthoringRigidBody>()
            .register_type::<ModelRef>()
            .register_type::<PrefabRef>()
            .register_type::<PrefabInstance>()
            .register_type::<WeaponSpawn>()
            .register_type::<TriggerVolume>()
            .register_type::<PostProcessVolumeDesc>()
            .register_type::<PostProcessVolumeOverrides>()
            .register_type::<TeamSpawn>()
            .register_type::<ObjectiveMarker>()
            .add_systems(Update, migrate_legacy_authoring_component_states)
            .add_plugins(HydrationPlugin);
    }
}

#[allow(clippy::type_complexity)]
fn migrate_legacy_authoring_component_states(
    mut commands: Commands,
    mut legacy: Query<
        (Entity, &mut InspectorOrder),
        (With<LevelObject>, Without<AuthoringComponentStates>),
    >,
) {
    for (entity, mut order) in &mut legacy {
        let mut states = std::mem::take(&mut order.component_states);
        for state in &mut states {
            if state.component_id.is_empty() {
                if let Some(component_id) = authoring_component_id(&state.type_name) {
                    state.component_id = component_id.to_string();
                    state.type_name.clear();
                }
            }
        }
        commands
            .entity(entity)
            .insert(AuthoringComponentStates { states });
    }
}

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

    #[test]
    fn gltf_standard_material_paths_use_pbr_conversion_label() {
        assert_eq!(
            standard_material_asset_path("models/robot.gltf", "Material3"),
            "models/robot.gltf#Material3/std"
        );
        assert_eq!(
            standard_material_asset_path("models/robot.GLB", "DefaultMaterial"),
            "models/robot.GLB#DefaultMaterial/std"
        );
    }

    #[test]
    fn non_gltf_material_paths_keep_loader_label() {
        assert_eq!(
            standard_material_asset_path("models/robot.fbx", "Material3"),
            "models/robot.fbx#Material3"
        );
    }

    #[test]
    fn converted_gltf_material_label_is_not_suffixed_twice() {
        assert_eq!(
            standard_material_asset_path("models/robot.glb", "Material3/std"),
            "models/robot.glb#Material3/std"
        );
    }

    #[test]
    fn legacy_inspector_active_state_moves_to_authoring_component_states() {
        let mut app = App::new();
        app.add_systems(Update, migrate_legacy_authoring_component_states);
        let mut order = InspectorOrder::default();
        order.component_states.push(InspectorComponentState {
            component_id: String::new(),
            type_name: COMPONENT_LIGHT_DESC.to_string(),
            active: false,
        });
        let entity = app.world_mut().spawn((LevelObject, order)).id();

        app.update();

        let states = app.world().get::<AuthoringComponentStates>(entity).unwrap();
        assert!(!states.is_component_active(COMPONENT_LIGHT_DESC));
        assert_eq!(states.states[0].component_id, AUTHORING_COMPONENT_LIGHT);
        assert!(states.states[0].type_name.is_empty());
        assert!(app
            .world()
            .get::<InspectorOrder>(entity)
            .unwrap()
            .component_states
            .is_empty());
    }

    #[test]
    fn new_active_state_takes_precedence_over_legacy_order() {
        let mut legacy = InspectorOrder::default();
        legacy.set_component_active(COMPONENT_LIGHT_DESC, false);
        let mut states = AuthoringComponentStates::default();
        states.set_component_active(COMPONENT_LIGHT_DESC, true);

        assert!(authoring_component_active(
            Some(&states),
            Some(&legacy),
            COMPONENT_LIGHT_DESC
        ));
    }

    #[test]
    fn inspector_order_migrates_legacy_type_paths_to_stable_ids_on_edit() {
        let present = [COMPONENT_LIGHT_DESC, COMPONENT_PRIMITIVE];
        let mut order = InspectorOrder {
            component_type_names: present.iter().map(|value| value.to_string()).collect(),
            ..Default::default()
        };

        assert_eq!(order.ordered_components(&present), present);
        assert!(order.move_component(COMPONENT_PRIMITIVE, -1, &present));
        assert!(order.component_type_names.is_empty());
        assert_eq!(
            order.component_ids,
            vec![
                AUTHORING_COMPONENT_PRIMITIVE.to_string(),
                AUTHORING_COMPONENT_LIGHT.to_string()
            ]
        );
        assert_eq!(
            order.ordered_components(&present),
            vec![COMPONENT_PRIMITIVE, COMPONENT_LIGHT_DESC]
        );
    }
}
