//! Selectable editor-only billboards at actor roots.

use std::collections::{HashMap, HashSet};

use bevy::asset::{
    load_internal_asset, load_internal_binary_asset, uuid_handle, RenderAssetUsages,
};
use bevy::camera::visibility::NoFrustumCulling;
use bevy::image::{CompressedImageFormats, ImageSampler, ImageType};
use bevy::pbr::{Material, MaterialPlugin};
use bevy::prelude::*;
use bevy::render::render_resource::{
    AsBindGroup, BlendState, CompareFunction, RenderPipelineDescriptor, ShaderType,
};
use bevy::shader::ShaderRef;
use shared::{
    ActorKind, AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, LevelObject,
    LightDesc, ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
    PrefabInstance, PrefabRef, Primitive, RaytracingExcluded, SkinnedMeshRenderer,
    StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
};

use crate::camera::EditorCamera;
use crate::infra::EditorOnly;
use crate::state::{EditorMode, PlayPossession};
use crate::ui::UiState;
use crate::viewport::ViewportDisplayMode;
use crate::visualizers::EditorVisualizationSettings;

const ICON_MIN_WORLD_SIZE: f32 = 0.001;
const ICON_MAX_WORLD_SIZE: f32 = 10_000.0;
const ICON_BRIGHTNESS: f32 = 1.6;
const DEFAULT_PERSPECTIVE_FOV: f32 = std::f32::consts::FRAC_PI_4;

const ACTOR_ICON_OVERLAY_SHADER: Handle<Shader> =
    uuid_handle!("2edfe544-3ed1-4e13-a880-f56b09804b52");
const ICON_01_ACTOR_EMPTY: Handle<Image> = uuid_handle!("d87c7796-b219-4011-8305-4db55422f1c7");
const ICON_02_ACTOR_GENERIC: Handle<Image> = uuid_handle!("c9a1a84f-4dcd-4a2b-a5b6-c365dc7d6795");
const ICON_03_ACTOR_MISSING: Handle<Image> = uuid_handle!("cca7cdf9-0c7d-4546-9bb9-1176e49a635c");
const ICON_04_ACTOR_WARNING: Handle<Image> = uuid_handle!("7dba1d9c-0569-40d2-978e-66014377051d");
const ICON_05_RENDER_MESH: Handle<Image> = uuid_handle!("10c70a1d-3327-458a-92d2-db41f893432e");
const ICON_06_RENDER_DECAL: Handle<Image> = uuid_handle!("8c27ff62-0d94-4184-81cf-977e652a0496");
const ICON_07_RENDER_PARTICLE: Handle<Image> = uuid_handle!("0db519d6-0eca-4389-9393-2e0c38f73119");
const ICON_08_RENDER_BILLBOARD: Handle<Image> =
    uuid_handle!("2fcb8e83-8f51-4978-8b29-146e2c44b5e6");
const ICON_09_LIGHT_POINT: Handle<Image> = uuid_handle!("ba2cbf62-c547-4cfe-ba61-4081076da93b");
const ICON_10_LIGHT_SPOT: Handle<Image> = uuid_handle!("200ee881-9cac-41d1-89be-45581642968f");
const ICON_11_LIGHT_DIRECTIONAL: Handle<Image> =
    uuid_handle!("d9de4730-2463-41dc-87e9-ed99b013d74a");
const ICON_12_LIGHT_AREA: Handle<Image> = uuid_handle!("d43ea6a8-15da-4792-8d63-962211f94ad6");
const ICON_13_CAMERA_MAIN: Handle<Image> = uuid_handle!("1979cc52-7285-4bb3-88f5-bd82021c10fa");
const ICON_14_CAMERA_CAPTURE: Handle<Image> = uuid_handle!("f1fc47d3-eaf4-4aaf-becc-b51a64f0b070");
const ICON_15_AUDIO_SOURCE: Handle<Image> = uuid_handle!("2dd3091f-2dea-4626-8430-79fa44a53c68");
const ICON_16_AUDIO_LISTENER: Handle<Image> = uuid_handle!("9eb3177f-e3d5-45cb-bd7c-49eecb4eb8e3");
const ICON_17_PHYSICS_COLLIDER: Handle<Image> =
    uuid_handle!("69f02ada-e171-484b-9a46-af753971e9c1");
const ICON_18_PHYSICS_RIGIDBODY: Handle<Image> =
    uuid_handle!("c30ddfb8-a851-45ab-98a9-97add45f1b49");
const ICON_19_PHYSICS_TRIGGER: Handle<Image> = uuid_handle!("3cf22740-463a-41b0-a1d7-d0cccc910b70");
const ICON_20_PHYSICS_JOINT: Handle<Image> = uuid_handle!("cdb8ef15-d41c-4f34-bc98-4a61f4628508");
const ICON_21_GAME_SPAWN: Handle<Image> = uuid_handle!("56719033-f1db-4749-8637-9b004581ce81");
const ICON_22_GAME_TRIGGER: Handle<Image> = uuid_handle!("98e42034-1c34-40af-8955-12c12cc2e261");
const ICON_23_GAME_MARKER: Handle<Image> = uuid_handle!("7808784d-c117-41a9-84db-fae5d4edbf3a");
const ICON_24_GAME_INTERACTABLE: Handle<Image> =
    uuid_handle!("e61fc53a-5201-49a4-9451-88d7c804a5c5");
const ICON_25_NAV_WAYPOINT: Handle<Image> = uuid_handle!("26511234-47bc-4cdc-b24c-39bfec7e46da");
const ICON_26_NAV_LINK: Handle<Image> = uuid_handle!("6f4988e9-b571-45af-a3ad-777d80a3d9b6");
const ICON_27_NAV_REGION: Handle<Image> = uuid_handle!("02b7ae0d-b3d6-4e46-8be0-8ee590ea71cc");
const ICON_28_VOLUME_GENERIC: Handle<Image> = uuid_handle!("670cf8ce-c33e-4f86-ae7e-0311c8917629");
const ICON_29_VOLUME_POST_PROCESS: Handle<Image> =
    uuid_handle!("bc9064bd-b6a8-4b00-95d4-3763325bceb9");
const ICON_30_ENVIRONMENT_WIND: Handle<Image> =
    uuid_handle!("0d4220a6-7abc-42bc-a0d2-b69847061a92");
const ICON_31_ENVIRONMENT_SKY: Handle<Image> = uuid_handle!("e4f09ad3-380d-4086-9eb4-e60e69ec1765");
const ICON_32_DEBUG_NOTE: Handle<Image> = uuid_handle!("d093e657-6598-4097-8a57-9d445e3a80d7");

#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ActorIconProxy {
    pub target: Entity,
}

#[derive(Resource, Debug, Clone)]
pub struct ActorIconSettings {
    pub size_pixels: f32,
}

impl ActorIconSettings {
    pub const DEFAULT_SIZE_PIXELS: f32 = 34.0;
    pub const MIN_SIZE_PIXELS: f32 = 16.0;
    pub const MAX_SIZE_PIXELS: f32 = 96.0;

    pub fn clamped_size_pixels(&self) -> f32 {
        self.size_pixels
            .clamp(Self::MIN_SIZE_PIXELS, Self::MAX_SIZE_PIXELS)
    }
}

impl Default for ActorIconSettings {
    fn default() -> Self {
        Self {
            size_pixels: Self::DEFAULT_SIZE_PIXELS,
        }
    }
}

#[derive(Clone, Copy, ShaderType)]
struct ActorIconOverlayUniform {
    brightness: f32,
}

#[derive(Asset, TypePath, AsBindGroup, Clone)]
struct ActorIconOverlayMaterial {
    #[texture(0)]
    #[sampler(1)]
    texture: Handle<Image>,
    #[uniform(2)]
    uniform: ActorIconOverlayUniform,
}

impl Material for ActorIconOverlayMaterial {
    fn fragment_shader() -> ShaderRef {
        ACTOR_ICON_OVERLAY_SHADER.into()
    }

    fn alpha_mode(&self) -> AlphaMode {
        AlphaMode::Blend
    }

    fn enable_prepass() -> bool {
        false
    }

    fn enable_shadows() -> bool {
        false
    }

    fn specialize(
        _pipeline: &bevy::pbr::MaterialPipeline,
        descriptor: &mut RenderPipelineDescriptor,
        _layout: &bevy::mesh::MeshVertexBufferLayoutRef,
        _key: bevy::pbr::MaterialPipelineKey<Self>,
    ) -> Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
        descriptor.primitive.cull_mode = None;
        if let Some(depth) = &mut descriptor.depth_stencil {
            depth.depth_write_enabled = Some(false);
            depth.depth_compare = Some(CompareFunction::Always);
        }
        if let Some(fragment) = &mut descriptor.fragment {
            if let Some(target) = fragment
                .targets
                .first_mut()
                .and_then(|target| target.as_mut())
            {
                target.blend = Some(BlendState::ALPHA_BLENDING);
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ActorIconImage {
    Empty,
    Generic,
    Missing,
    Warning,
    Mesh,
    LightPoint,
    LightSpot,
    LightDirectional,
    AudioSource,
    AudioListener,
    PhysicsCollider,
    GameSpawn,
    GameTrigger,
    GameMarker,
    VolumeGeneric,
    PostProcessVolume,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ActorIconCategory {
    Actor,
    Mesh,
    Light,
    Audio,
    Physics,
    Gameplay,
    Volume,
    PrefabModel,
    Debug,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ActorIconSpec {
    image: ActorIconImage,
    category: ActorIconCategory,
}

#[derive(Default, Debug, Clone, Copy)]
struct ActorIconComponents {
    kind: Option<ActorKind>,
    light_kind: Option<AuthoringLightKind>,
    has_primitive: bool,
    has_static_mesh_renderer: bool,
    has_skinned_mesh_renderer: bool,
    has_model_ref: bool,
    has_prefab_ref: bool,
    has_prefab_instance: bool,
    has_player_spawn: bool,
    has_weapon_spawn: bool,
    has_trigger_volume: bool,
    has_post_process_volume: bool,
    has_team_spawn: bool,
    has_objective: bool,
    has_audio_source: bool,
    has_audio_listener: bool,
    has_physics: bool,
}

#[derive(Resource)]
struct ActorIconAssets {
    mesh: Handle<Mesh>,
    materials: HashMap<ActorIconImage, Handle<ActorIconOverlayMaterial>>,
}

pub struct ActorIconsPlugin;

impl Plugin for ActorIconsPlugin {
    fn build(&self, app: &mut App) {
        register_actor_icon_images(app);
        load_internal_asset!(
            app,
            ACTOR_ICON_OVERLAY_SHADER,
            "../../assets/shaders/actor_icon_overlay.wgsl",
            Shader::from_wgsl
        );

        app.init_resource::<ActorIconSettings>()
            .add_plugins(MaterialPlugin::<ActorIconOverlayMaterial>::default())
            .add_systems(Startup, init_actor_icon_assets)
            .add_systems(Update, sync_actor_icons);
    }
}

fn icon_image_from_png(bytes: &[u8], path: String) -> Image {
    Image::from_buffer(
        bytes,
        ImageType::Extension("png"),
        CompressedImageFormats::NONE,
        true,
        ImageSampler::linear(),
        RenderAssetUsages::default(),
    )
    .unwrap_or_else(|err| panic!("failed to load editor actor icon {path}: {err}"))
}

macro_rules! load_actor_icon {
    ($app:ident, $handle:expr, $path:literal) => {
        load_internal_binary_asset!(
            $app,
            $handle,
            concat!("../../assets/icons/", $path),
            icon_image_from_png
        );
    };
}

fn register_actor_icon_images(app: &mut App) {
    load_actor_icon!(app, ICON_01_ACTOR_EMPTY, "01_actor_empty.png");
    load_actor_icon!(app, ICON_02_ACTOR_GENERIC, "02_actor_generic.png");
    load_actor_icon!(app, ICON_03_ACTOR_MISSING, "03_actor_missing.png");
    load_actor_icon!(app, ICON_04_ACTOR_WARNING, "04_actor_warning.png");
    load_actor_icon!(app, ICON_05_RENDER_MESH, "05_render_mesh.png");
    load_actor_icon!(app, ICON_06_RENDER_DECAL, "06_render_decal.png");
    load_actor_icon!(app, ICON_07_RENDER_PARTICLE, "07_render_particle.png");
    load_actor_icon!(app, ICON_08_RENDER_BILLBOARD, "08_render_billboard.png");
    load_actor_icon!(app, ICON_09_LIGHT_POINT, "09_light_point.png");
    load_actor_icon!(app, ICON_10_LIGHT_SPOT, "10_light_spot.png");
    load_actor_icon!(app, ICON_11_LIGHT_DIRECTIONAL, "11_light_directional.png");
    load_actor_icon!(app, ICON_12_LIGHT_AREA, "12_light_area.png");
    load_actor_icon!(app, ICON_13_CAMERA_MAIN, "13_camera_main.png");
    load_actor_icon!(app, ICON_14_CAMERA_CAPTURE, "14_camera_capture.png");
    load_actor_icon!(app, ICON_15_AUDIO_SOURCE, "15_audio_source.png");
    load_actor_icon!(app, ICON_16_AUDIO_LISTENER, "16_audio_listener.png");
    load_actor_icon!(app, ICON_17_PHYSICS_COLLIDER, "17_physics_collider.png");
    load_actor_icon!(app, ICON_18_PHYSICS_RIGIDBODY, "18_physics_rigidbody.png");
    load_actor_icon!(app, ICON_19_PHYSICS_TRIGGER, "19_physics_trigger.png");
    load_actor_icon!(app, ICON_20_PHYSICS_JOINT, "20_physics_joint.png");
    load_actor_icon!(app, ICON_21_GAME_SPAWN, "21_game_spawn.png");
    load_actor_icon!(app, ICON_22_GAME_TRIGGER, "22_game_trigger.png");
    load_actor_icon!(app, ICON_23_GAME_MARKER, "23_game_marker.png");
    load_actor_icon!(app, ICON_24_GAME_INTERACTABLE, "24_game_interactable.png");
    load_actor_icon!(app, ICON_25_NAV_WAYPOINT, "25_nav_waypoint.png");
    load_actor_icon!(app, ICON_26_NAV_LINK, "26_nav_link.png");
    load_actor_icon!(app, ICON_27_NAV_REGION, "27_nav_region.png");
    load_actor_icon!(app, ICON_28_VOLUME_GENERIC, "28_volume_generic.png");
    load_actor_icon!(
        app,
        ICON_29_VOLUME_POST_PROCESS,
        "29_volume_post_process.png"
    );
    load_actor_icon!(app, ICON_30_ENVIRONMENT_WIND, "30_environment_wind.png");
    load_actor_icon!(app, ICON_31_ENVIRONMENT_SKY, "31_environment_sky.png");
    load_actor_icon!(app, ICON_32_DEBUG_NOTE, "32_debug_note.png");
}

fn init_actor_icon_assets(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ActorIconOverlayMaterial>>,
) {
    let mesh = meshes.add(Rectangle::new(1.0, 1.0));
    let mut actor_materials = HashMap::new();
    for (image, texture) in [
        (ActorIconImage::Empty, ICON_01_ACTOR_EMPTY),
        (ActorIconImage::Generic, ICON_02_ACTOR_GENERIC),
        (ActorIconImage::Missing, ICON_03_ACTOR_MISSING),
        (ActorIconImage::Warning, ICON_04_ACTOR_WARNING),
        (ActorIconImage::Mesh, ICON_05_RENDER_MESH),
        (ActorIconImage::LightPoint, ICON_09_LIGHT_POINT),
        (ActorIconImage::LightSpot, ICON_10_LIGHT_SPOT),
        (ActorIconImage::LightDirectional, ICON_11_LIGHT_DIRECTIONAL),
        (ActorIconImage::AudioSource, ICON_15_AUDIO_SOURCE),
        (ActorIconImage::AudioListener, ICON_16_AUDIO_LISTENER),
        (ActorIconImage::PhysicsCollider, ICON_17_PHYSICS_COLLIDER),
        (ActorIconImage::GameSpawn, ICON_21_GAME_SPAWN),
        (ActorIconImage::GameTrigger, ICON_22_GAME_TRIGGER),
        (ActorIconImage::GameMarker, ICON_23_GAME_MARKER),
        (ActorIconImage::VolumeGeneric, ICON_28_VOLUME_GENERIC),
        (
            ActorIconImage::PostProcessVolume,
            ICON_29_VOLUME_POST_PROCESS,
        ),
    ] {
        actor_materials.insert(
            image,
            materials.add(ActorIconOverlayMaterial {
                texture: texture.clone(),
                uniform: ActorIconOverlayUniform {
                    brightness: ICON_BRIGHTNESS,
                },
            }),
        );
    }

    commands.insert_resource(ActorIconAssets {
        mesh,
        materials: actor_materials,
    });
}

#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn sync_actor_icons(
    mode: Res<State<EditorMode>>,
    possession: Res<PlayPossession>,
    display: Res<ViewportDisplayMode>,
    settings: Res<EditorVisualizationSettings>,
    icon_settings: Res<ActorIconSettings>,
    ui_state: Res<UiState>,
    assets: Res<ActorIconAssets>,
    mut commands: Commands,
    mut icons: Query<(
        Entity,
        &ActorIconProxy,
        &MeshMaterial3d<ActorIconOverlayMaterial>,
        &mut Transform,
    )>,
    camera_query: Query<(&Camera, &GlobalTransform, Option<&Projection>), With<EditorCamera>>,
    actor_roots: Query<
        (
            Entity,
            &GlobalTransform,
            Option<&ActorKind>,
            Option<&LightDesc>,
            Has<Primitive>,
            Option<&StaticMeshRenderer>,
            Has<ModelRef>,
            Has<PrefabRef>,
            Has<PrefabInstance>,
            Has<PlayerSpawn>,
            Has<WeaponSpawn>,
            Has<TriggerVolume>,
            Has<PostProcessVolumeDesc>,
            Has<TeamSpawn>,
            Has<ObjectiveMarker>,
        ),
        (With<LevelObject>, Without<EditorOnly>),
    >,
    skinned_meshes: Query<(), With<SkinnedMeshRenderer>>,
    audio: Query<(Has<AudioSourceDesc>, Has<AudioListenerDesc>)>,
    physics: Query<(), Or<(With<PhysicsBody>, With<ColliderDesc>)>>,
) {
    let camera = active_editor_camera(&camera_query);
    let mut desired = HashMap::new();
    let tools_active = *mode.get() == EditorMode::Editing
        || (*mode.get() == EditorMode::Playing && *possession == PlayPossession::Ejected);
    if tools_active && !display.clean_game_view && settings.enabled && settings.show_actor_icons {
        for (
            target,
            global,
            kind,
            light,
            has_primitive,
            static_mesh_renderer,
            has_model_ref,
            has_prefab_ref,
            has_prefab_instance,
            has_player_spawn,
            has_weapon_spawn,
            has_trigger_volume,
            has_post_process_volume,
            has_team_spawn,
            has_objective,
        ) in &actor_roots
        {
            let (has_audio_source, has_audio_listener) = audio.get(target).unwrap_or_default();
            let components = ActorIconComponents {
                kind: kind.copied(),
                light_kind: light.map(|light| light.kind),
                has_primitive,
                has_static_mesh_renderer: static_mesh_renderer
                    .is_some_and(has_resolved_static_mesh_slot),
                has_skinned_mesh_renderer: skinned_meshes.contains(target),
                has_model_ref,
                has_prefab_ref,
                has_prefab_instance,
                has_player_spawn,
                has_weapon_spawn,
                has_trigger_volume,
                has_post_process_volume,
                has_team_spawn,
                has_objective,
                has_audio_source,
                has_audio_listener,
                has_physics: physics.contains(target),
            };
            let spec = icon_for_actor_components(components);
            if !icon_category_enabled(&settings, spec.category) {
                continue;
            }
            let transform = desired_icon_transform(
                global,
                camera,
                ui_state.viewport_rect.height(),
                icon_settings.clamped_size_pixels(),
            );
            desired.insert(target, (spec.image, transform));
        }
    }

    let mut existing_targets = HashSet::new();
    for (entity, proxy, material, mut transform) in &mut icons {
        let Some((image, desired_transform)) = desired.get(&proxy.target) else {
            commands.entity(entity).despawn();
            continue;
        };
        let expected_material = assets.materials.get(image);
        if expected_material.is_none_or(|expected| material.0 != *expected) {
            commands.entity(entity).despawn();
            continue;
        }
        existing_targets.insert(proxy.target);
        *transform = *desired_transform;
    }

    for (
        target,
        global,
        kind,
        light,
        has_primitive,
        static_mesh_renderer,
        has_model_ref,
        has_prefab_ref,
        has_prefab_instance,
        has_player_spawn,
        has_weapon_spawn,
        has_trigger_volume,
        has_post_process_volume,
        has_team_spawn,
        has_objective,
    ) in &actor_roots
    {
        if existing_targets.contains(&target) {
            continue;
        }
        let (has_audio_source, has_audio_listener) = audio.get(target).unwrap_or_default();
        let components = ActorIconComponents {
            kind: kind.copied(),
            light_kind: light.map(|light| light.kind),
            has_primitive,
            has_static_mesh_renderer: static_mesh_renderer
                .is_some_and(has_resolved_static_mesh_slot),
            has_skinned_mesh_renderer: skinned_meshes.contains(target),
            has_model_ref,
            has_prefab_ref,
            has_prefab_instance,
            has_player_spawn,
            has_weapon_spawn,
            has_trigger_volume,
            has_post_process_volume,
            has_team_spawn,
            has_objective,
            has_audio_source,
            has_audio_listener,
            has_physics: physics.contains(target),
        };
        let spec = icon_for_actor_components(components);
        if !desired.contains_key(&target) {
            continue;
        }
        let Some(material) = assets.materials.get(&spec.image).cloned() else {
            continue;
        };
        let transform = desired_icon_transform(
            global,
            camera,
            ui_state.viewport_rect.height(),
            icon_settings.clamped_size_pixels(),
        );
        commands.spawn((
            EditorOnly,
            RaytracingExcluded,
            NoFrustumCulling,
            Name::new("Actor Root Icon"),
            ActorIconProxy { target },
            Mesh3d(assets.mesh.clone()),
            MeshMaterial3d(material),
            transform,
            Visibility::Visible,
            ChildOf(target),
        ));
    }
}

fn active_editor_camera<'a>(
    camera_query: &'a Query<(&Camera, &GlobalTransform, Option<&Projection>), With<EditorCamera>>,
) -> Option<(&'a Camera, &'a GlobalTransform, Option<&'a Projection>)> {
    camera_query
        .iter()
        .find(|(camera, _, _)| camera.is_active)
        .or_else(|| camera_query.iter().next())
}

fn desired_icon_transform(
    parent_global: &GlobalTransform,
    camera: Option<(&Camera, &GlobalTransform, Option<&Projection>)>,
    viewport_height: f32,
    target_pixels: f32,
) -> Transform {
    let translation = parent_global.translation();
    let Some((_, camera_global, projection)) = camera else {
        return Transform::from_translation(Vec3::ZERO).with_scale(Vec3::splat(0.5));
    };
    let size = icon_world_size(
        translation,
        camera_global,
        projection,
        viewport_height.max(1.0),
        target_pixels,
    );
    let desired_global = Transform {
        translation,
        rotation: camera_global.rotation(),
        scale: Vec3::splat(size),
    };
    let local = parent_global.affine().inverse() * GlobalTransform::from(desired_global).affine();
    Transform::from_matrix(Mat4::from(local))
}

fn icon_world_size(
    target: Vec3,
    camera_global: &GlobalTransform,
    projection: Option<&Projection>,
    viewport_height: f32,
    target_pixels: f32,
) -> f32 {
    let pixels_to_view = target_pixels / viewport_height;
    let world_height = match projection {
        Some(Projection::Perspective(projection)) => {
            let target_view = camera_global.affine().inverse().transform_point3(target);
            let depth = (-target_view.z).abs().max(0.1);
            2.0 * depth * (projection.fov * 0.5).tan()
        }
        Some(Projection::Orthographic(projection)) => projection.area.height().abs().max(0.1),
        _ => {
            let target_view = camera_global.affine().inverse().transform_point3(target);
            let depth = (-target_view.z).abs().max(0.1);
            2.0 * depth * (DEFAULT_PERSPECTIVE_FOV * 0.5).tan()
        }
    };
    (world_height * pixels_to_view).clamp(ICON_MIN_WORLD_SIZE, ICON_MAX_WORLD_SIZE)
}

fn icon_category_enabled(
    settings: &EditorVisualizationSettings,
    category: ActorIconCategory,
) -> bool {
    match category {
        ActorIconCategory::Actor => settings.show_actor_icon_actor,
        ActorIconCategory::Mesh => settings.show_actor_icon_mesh,
        ActorIconCategory::Light => settings.show_actor_icon_light,
        ActorIconCategory::Audio => settings.show_actor_icon_audio,
        ActorIconCategory::Physics => settings.show_actor_icon_physics,
        ActorIconCategory::Gameplay => settings.show_actor_icon_gameplay,
        ActorIconCategory::Volume => settings.show_actor_icon_volume,
        ActorIconCategory::PrefabModel => settings.show_actor_icon_prefab_model,
        ActorIconCategory::Debug => settings.show_actor_icon_debug,
    }
}

fn has_resolved_static_mesh_slot(renderer: &StaticMeshRenderer) -> bool {
    renderer.slots.iter().any(|slot| slot.mesh.is_resolved())
}

fn icon_for_actor_components(components: ActorIconComponents) -> ActorIconSpec {
    let warning = ActorIconSpec {
        image: ActorIconImage::Warning,
        category: ActorIconCategory::Debug,
    };
    let Some(kind) = components.kind else {
        return ActorIconSpec {
            image: ActorIconImage::Missing,
            category: ActorIconCategory::Debug,
        };
    };

    match kind {
        ActorKind::Brush => ActorIconSpec {
            image: ActorIconImage::Mesh,
            category: ActorIconCategory::Mesh,
        },
        ActorKind::StaticMesh => {
            if components.has_primitive || components.has_static_mesh_renderer {
                ActorIconSpec {
                    image: ActorIconImage::Mesh,
                    category: ActorIconCategory::Mesh,
                }
            } else {
                warning
            }
        }
        ActorKind::SkinnedMesh => {
            if components.has_skinned_mesh_renderer {
                ActorIconSpec {
                    image: ActorIconImage::Mesh,
                    category: ActorIconCategory::Mesh,
                }
            } else {
                warning
            }
        }
        ActorKind::ImportedModel => {
            if components.has_model_ref {
                ActorIconSpec {
                    image: ActorIconImage::Mesh,
                    category: ActorIconCategory::PrefabModel,
                }
            } else {
                warning
            }
        }
        ActorKind::Light => match components.light_kind {
            Some(AuthoringLightKind::Point) => ActorIconSpec {
                image: ActorIconImage::LightPoint,
                category: ActorIconCategory::Light,
            },
            Some(AuthoringLightKind::Spot) => ActorIconSpec {
                image: ActorIconImage::LightSpot,
                category: ActorIconCategory::Light,
            },
            Some(AuthoringLightKind::Directional) => ActorIconSpec {
                image: ActorIconImage::LightDirectional,
                category: ActorIconCategory::Light,
            },
            None => warning,
        },
        ActorKind::PrefabAnchor => {
            if components.has_prefab_ref || components.has_prefab_instance {
                ActorIconSpec {
                    image: ActorIconImage::Generic,
                    category: ActorIconCategory::PrefabModel,
                }
            } else {
                warning
            }
        }
        ActorKind::PlayerSpawn => {
            if components.has_player_spawn {
                ActorIconSpec {
                    image: ActorIconImage::GameSpawn,
                    category: ActorIconCategory::Gameplay,
                }
            } else {
                warning
            }
        }
        ActorKind::WeaponSpawn => {
            if components.has_weapon_spawn {
                ActorIconSpec {
                    image: ActorIconImage::GameSpawn,
                    category: ActorIconCategory::Gameplay,
                }
            } else {
                warning
            }
        }
        ActorKind::TriggerVolume => {
            if components.has_trigger_volume {
                ActorIconSpec {
                    image: ActorIconImage::GameTrigger,
                    category: ActorIconCategory::Gameplay,
                }
            } else {
                warning
            }
        }
        ActorKind::PostProcessVolume => {
            if components.has_post_process_volume {
                ActorIconSpec {
                    image: ActorIconImage::PostProcessVolume,
                    category: ActorIconCategory::Volume,
                }
            } else {
                warning
            }
        }
        ActorKind::TeamSpawn => {
            if components.has_team_spawn {
                ActorIconSpec {
                    image: ActorIconImage::GameSpawn,
                    category: ActorIconCategory::Gameplay,
                }
            } else {
                warning
            }
        }
        ActorKind::Objective => {
            if components.has_objective {
                ActorIconSpec {
                    image: ActorIconImage::GameMarker,
                    category: ActorIconCategory::Gameplay,
                }
            } else {
                warning
            }
        }
        ActorKind::AudioSource => {
            if components.has_audio_source {
                ActorIconSpec {
                    image: ActorIconImage::AudioSource,
                    category: ActorIconCategory::Audio,
                }
            } else {
                warning
            }
        }
        ActorKind::AudioListener => {
            if components.has_audio_listener {
                ActorIconSpec {
                    image: ActorIconImage::AudioListener,
                    category: ActorIconCategory::Audio,
                }
            } else {
                warning
            }
        }
        ActorKind::Navigation => ActorIconSpec {
            image: ActorIconImage::GameMarker,
            category: ActorIconCategory::Volume,
        },
        ActorKind::Empty => {
            if components.has_physics {
                ActorIconSpec {
                    image: ActorIconImage::PhysicsCollider,
                    category: ActorIconCategory::Physics,
                }
            } else {
                ActorIconSpec {
                    image: ActorIconImage::Empty,
                    category: ActorIconCategory::Actor,
                }
            }
        }
    }
}

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

    #[test]
    fn resolves_directional_light_icon() {
        let spec = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::Light),
            light_kind: Some(AuthoringLightKind::Directional),
            ..default()
        });

        assert_eq!(spec.image, ActorIconImage::LightDirectional);
        assert_eq!(spec.category, ActorIconCategory::Light);
    }

    #[test]
    fn unresolved_static_mesh_uses_warning_icon() {
        let spec = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::StaticMesh),
            ..default()
        });

        assert_eq!(spec.image, ActorIconImage::Warning);
        assert_eq!(spec.category, ActorIconCategory::Debug);
    }

    #[test]
    fn empty_actor_with_collider_uses_physics_category() {
        let spec = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::Empty),
            has_physics: true,
            ..default()
        });

        assert_eq!(spec.image, ActorIconImage::PhysicsCollider);
        assert_eq!(spec.category, ActorIconCategory::Physics);
    }

    #[test]
    fn audio_source_and_listener_use_dedicated_icons() {
        let source = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::AudioSource),
            has_audio_source: true,
            ..default()
        });
        let listener = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::AudioListener),
            has_audio_listener: true,
            ..default()
        });

        assert_eq!(source.image, ActorIconImage::AudioSource);
        assert_eq!(source.category, ActorIconCategory::Audio);
        assert_eq!(listener.image, ActorIconImage::AudioListener);
        assert_eq!(listener.category, ActorIconCategory::Audio);
    }

    #[test]
    fn audio_actor_without_matching_descriptor_uses_warning_icon() {
        let spec = icon_for_actor_components(ActorIconComponents {
            kind: Some(ActorKind::AudioSource),
            ..default()
        });

        assert_eq!(spec.image, ActorIconImage::Warning);
        assert_eq!(spec.category, ActorIconCategory::Debug);
    }

    #[test]
    fn audio_icon_category_respects_its_viewport_toggle() {
        let mut settings = EditorVisualizationSettings::default();
        assert!(icon_category_enabled(&settings, ActorIconCategory::Audio));

        settings.show_actor_icon_audio = false;
        assert!(!icon_category_enabled(&settings, ActorIconCategory::Audio));
    }

    #[test]
    fn perspective_icon_size_uses_view_depth() {
        let camera = GlobalTransform::IDENTITY;
        let projection = Projection::Perspective(PerspectiveProjection {
            fov: DEFAULT_PERSPECTIVE_FOV,
            ..default()
        });
        let centered = icon_world_size(
            Vec3::new(0.0, 0.0, -12.0),
            &camera,
            Some(&projection),
            900.0,
            ActorIconSettings::DEFAULT_SIZE_PIXELS,
        );
        let offset = icon_world_size(
            Vec3::new(8.0, 0.0, -12.0),
            &camera,
            Some(&projection),
            900.0,
            ActorIconSettings::DEFAULT_SIZE_PIXELS,
        );

        assert_eq!(centered, offset);
    }
}
