//! Editor-side scene visualizers for authoring/runtime entities that have no mesh.

use avian3d::prelude::ColliderConstructor;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use shared::{
    AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, ColliderShapeDesc,
    LevelObject, LightDesc, ModelRef, NavigationArea, NavigationBounds, NavigationLink,
    NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
    PrefabRef, ProjectSun, RaytracingExcluded, SkinnedMeshRenderer, TeamSpawn, TriggerVolume,
    WeaponSpawn,
};
use sim::tuning::{EYE_OFFSET_STAND, PLAYER_LENGTH, PLAYER_RADIUS};
use sim::{Player, PlayerCamera};

use crate::infra::EditorOnly;
use crate::selection::SelectedEntity;
use crate::state::{scene_tools_active, EditorMode, PlayPossession};
use crate::viewport::{EditorViewportMode, ViewportDisplayMode};

#[derive(Resource, Debug, Clone)]
pub struct EditorVisualizationSettings {
    pub enabled: bool,
    pub show_actor_icons: bool,
    pub show_actor_icon_actor: bool,
    pub show_actor_icon_mesh: bool,
    pub show_actor_icon_light: bool,
    pub show_actor_icon_audio: bool,
    pub show_actor_icon_physics: bool,
    pub show_actor_icon_gameplay: bool,
    pub show_actor_icon_volume: bool,
    pub show_actor_icon_prefab_model: bool,
    pub show_actor_icon_debug: bool,
    pub show_colliders: bool,
    pub show_lights: bool,
    pub show_audio: bool,
    pub show_spawns: bool,
    pub show_gameplay: bool,
    pub show_volumes: bool,
    pub show_prefab_model_anchors: bool,
    pub show_runtime: bool,
    pub pickable_proxies: bool,
}

impl Default for EditorVisualizationSettings {
    fn default() -> Self {
        Self {
            enabled: true,
            show_actor_icons: true,
            show_actor_icon_actor: true,
            show_actor_icon_mesh: true,
            show_actor_icon_light: true,
            show_actor_icon_audio: true,
            show_actor_icon_physics: true,
            show_actor_icon_gameplay: true,
            show_actor_icon_volume: true,
            show_actor_icon_prefab_model: true,
            show_actor_icon_debug: true,
            show_colliders: true,
            show_lights: true,
            show_audio: true,
            show_spawns: true,
            show_gameplay: true,
            show_volumes: true,
            show_prefab_model_anchors: true,
            show_runtime: true,
            pickable_proxies: true,
        }
    }
}

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

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VisualizerKind {
    Collider,
    Light,
    PlayerSpawn,
    Prefab,
    Model,
    Player,
    Camera,
    ProjectSun,
}

#[derive(Resource)]
struct VisualizerProxyAssets {
    mesh: Handle<Mesh>,
    material: Handle<StandardMaterial>,
}

#[derive(SystemParam)]
struct VisualizerTargetQueries<'w, 's> {
    transforms: Query<'w, 's, &'static GlobalTransform>,
    colliders: Query<
        'w,
        's,
        Entity,
        (
            With<LevelObject>,
            Or<(With<PhysicsBody>, With<ColliderDesc>)>,
        ),
    >,
    lights: Query<'w, 's, Entity, (With<LevelObject>, With<LightDesc>)>,
    spawns: Query<'w, 's, Entity, (With<LevelObject>, With<PlayerSpawn>)>,
    prefabs: Query<'w, 's, Entity, (With<LevelObject>, With<PrefabRef>)>,
    models: Query<
        'w,
        's,
        Entity,
        (
            With<LevelObject>,
            Or<(With<ModelRef>, With<SkinnedMeshRenderer>)>,
        ),
    >,
    players: Query<'w, 's, Entity, With<Player>>,
    cameras: Query<'w, 's, Entity, With<PlayerCamera>>,
    project_suns: Query<'w, 's, Entity, With<ProjectSun>>,
}

pub struct EditorVisualizersPlugin;

impl Plugin for EditorVisualizersPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<EditorVisualizationSettings>()
            .add_systems(Startup, init_visualizer_proxy_assets)
            .add_systems(Update, sync_visualizer_proxies)
            .add_systems(
                Update,
                (
                    draw_visualizers,
                    draw_audio_visualizers,
                    draw_split_collider_visualizers,
                    draw_post_process_volume_gizmos,
                    draw_navigation_visualizers,
                )
                    .chain()
                    .run_if(scene_tools_active)
                    .run_if(viewport_editor_overlays_visible),
            );
    }
}

fn draw_navigation_visualizers(
    state: Res<crate::ui::navigation_inspector::NavigationEditorState>,
    mut gizmos: Gizmos,
    bounds: Query<(&NavigationBounds, &GlobalTransform), With<LevelObject>>,
    obstacles: Query<(&NavigationObstacle, &GlobalTransform), With<LevelObject>>,
    areas: Query<(&NavigationArea, &GlobalTransform), With<LevelObject>>,
    links: Query<(&NavigationLink, &GlobalTransform), With<LevelObject>>,
) {
    for (value, global) in &bounds {
        let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
            global.affine(),
            value.half_extents,
        );
        let center = global.translation();
        draw_box(
            &mut gizmos,
            center,
            Quat::IDENTITY,
            half_extents,
            Color::srgba(0.12, 0.82, 0.78, 0.72),
        );
    }
    for (value, global) in &obstacles {
        let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
            global.affine(),
            value.half_extents,
        );
        let center = global.translation();
        draw_box(
            &mut gizmos,
            center,
            Quat::IDENTITY,
            half_extents,
            Color::srgba(1.0, 0.28, 0.22, 0.78),
        );
    }
    for (value, global) in &areas {
        let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
            global.affine(),
            value.half_extents,
        );
        let center = global.translation();
        let color = if value.walkable {
            Color::srgba(0.32, 0.72, 1.0, 0.62)
        } else {
            Color::srgba(1.0, 0.55, 0.18, 0.72)
        };
        draw_box(&mut gizmos, center, Quat::IDENTITY, half_extents, color);
    }
    for (value, global) in &links {
        let start = global.transform_point(value.start);
        let end = global.transform_point(value.end);
        let color = if value.enabled {
            Color::srgba(0.95, 0.78, 0.22, 0.95)
        } else {
            Color::srgba(0.48, 0.48, 0.5, 0.55)
        };
        gizmos.line(start, end, color);
        gizmos
            .sphere(Isometry3d::from_translation(start), 0.11, color)
            .resolution(12);
        gizmos
            .sphere(Isometry3d::from_translation(end), 0.11, color)
            .resolution(12);
    }

    if state.show_mesh {
        if let Some(artifact) = &state.artifact {
            for layer in &artifact.mesh.layers {
                for polygon in &layer.polygons {
                    for index in 0..polygon.vertices.len() {
                        let a_index = polygon.vertices[index] as usize;
                        let b_index =
                            polygon.vertices[(index + 1) % polygon.vertices.len()] as usize;
                        let Some(a) = navigation_vertex(layer, a_index) else {
                            continue;
                        };
                        let Some(b) = navigation_vertex(layer, b_index) else {
                            continue;
                        };
                        gizmos.line(
                            a + Vec3::Y * 0.025,
                            b + Vec3::Y * 0.025,
                            Color::srgba(0.08, 0.9, 0.72, 0.7),
                        );
                    }
                }
            }
        }
    }

    for edge in state.preview_path.windows(2) {
        gizmos.line(
            edge[0] + Vec3::Y * 0.08,
            edge[1] + Vec3::Y * 0.08,
            Color::srgba(0.98, 0.88, 0.2, 1.0),
        );
    }
    if let Some(start) = state.preview_path.first().copied() {
        gizmos
            .sphere(
                Isometry3d::from_translation(start + Vec3::Y * 0.08),
                0.16,
                Color::srgba(0.2, 1.0, 0.55, 1.0),
            )
            .resolution(14);
    }
    if let Some(end) = state.preview_path.last().copied() {
        gizmos
            .sphere(
                Isometry3d::from_translation(end + Vec3::Y * 0.08),
                0.16,
                Color::srgba(1.0, 0.28, 0.22, 1.0),
            )
            .resolution(14);
    }
}

fn navigation_vertex(layer: &polyanya::Layer, index: usize) -> Option<Vec3> {
    let vertex = layer.vertices.get(index)?;
    let height = layer.height.get(index).copied().unwrap_or_default();
    let coords = vertex.coords + layer.offset;
    Some(Vec3::new(coords.x, height, coords.y))
}

fn draw_audio_visualizers(
    viewport_mode: Res<EditorViewportMode>,
    settings: Res<EditorVisualizationSettings>,
    selected: Res<SelectedEntity>,
    mut gizmos: Gizmos,
    sources: Query<(Entity, &AudioSourceDesc, &GlobalTransform), With<LevelObject>>,
    listeners: Query<(Entity, &AudioListenerDesc, &GlobalTransform), With<LevelObject>>,
) {
    if *viewport_mode == EditorViewportMode::Collider || !settings.enabled || !settings.show_audio {
        return;
    }
    let Some(selected) = selected.0 else {
        return;
    };

    if let Ok((_, source, global)) = sources.get(selected) {
        draw_audio_source(&mut gizmos, global, source);
    }
    if let Ok((_, listener, global)) = listeners.get(selected) {
        draw_audio_listener(&mut gizmos, global, listener);
    }
}

fn draw_audio_source(gizmos: &mut Gizmos, global: &GlobalTransform, source: &AudioSourceDesc) {
    let center = global.translation();
    let (min_distance, max_distance) = normalized_audio_radii(
        source.attenuation.min_distance,
        source.attenuation.max_distance,
    );
    let blend = source.spatial_blend.clamp(0.0, 1.0);
    let inner = Color::srgba(0.28, 0.92, 1.0, 0.9);
    let outer = Color::srgba(0.22, 0.68, 1.0, 0.28 + blend * 0.32);

    gizmos
        .sphere(
            Isometry3d::new(center, global.rotation()),
            min_distance,
            inner,
        )
        .resolution(28);
    gizmos
        .sphere(
            Isometry3d::new(center, global.rotation()),
            max_distance,
            outer,
        )
        .resolution(36);
}

fn draw_audio_listener(
    gizmos: &mut Gizmos,
    global: &GlobalTransform,
    listener: &AudioListenerDesc,
) {
    let geometry =
        audio_listener_geometry(global.translation(), global.rotation(), listener.ear_gap);
    let color = if listener.enabled {
        Color::srgba(0.35, 1.0, 0.76, 1.0)
    } else {
        Color::srgba(0.55, 0.62, 0.66, 0.75)
    };

    gizmos.line(geometry.left_ear, geometry.right_ear, color);
    for ear in [geometry.left_ear, geometry.right_ear] {
        gizmos
            .sphere(Isometry3d::from_translation(ear), 0.07, color)
            .resolution(12);
    }
    gizmos.arrow(geometry.center, geometry.forward_end, color);
    gizmos
        .circle(
            Isometry3d::new(geometry.center, global.rotation()),
            geometry.head_radius,
            color.with_alpha(0.55),
        )
        .resolution(24);
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct AudioListenerGeometry {
    center: Vec3,
    left_ear: Vec3,
    right_ear: Vec3,
    forward_end: Vec3,
    head_radius: f32,
}

fn normalized_audio_radii(min_distance: f32, max_distance: f32) -> (f32, f32) {
    let min_distance = if min_distance.is_finite() {
        min_distance.max(0.05)
    } else {
        0.05
    };
    let max_distance = if max_distance.is_finite() {
        max_distance.max(min_distance)
    } else {
        min_distance
    };
    (min_distance, max_distance)
}

fn audio_listener_geometry(center: Vec3, rotation: Quat, ear_gap: f32) -> AudioListenerGeometry {
    let ear_gap = if ear_gap.is_finite() {
        ear_gap.max(0.02)
    } else {
        0.02
    };
    let right = rotation * Vec3::X;
    let forward = rotation * Vec3::NEG_Z;
    AudioListenerGeometry {
        center,
        left_ear: center - right * ear_gap * 0.5,
        right_ear: center + right * ear_gap * 0.5,
        forward_end: center + forward * 0.85,
        head_radius: (ear_gap * 0.65).max(0.14),
    }
}

fn viewport_editor_overlays_visible(display: Res<ViewportDisplayMode>) -> bool {
    !display.clean_game_view
}

fn init_visualizer_proxy_assets(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.insert_resource(VisualizerProxyAssets {
        mesh: meshes.add(Sphere::new(0.22).mesh().uv(16, 8)),
        material: materials.add(StandardMaterial {
            base_color: Color::srgba(0.2, 0.8, 1.0, 0.08),
            alpha_mode: AlphaMode::Blend,
            unlit: true,
            ..default()
        }),
    });
}

#[allow(clippy::too_many_arguments)]
fn sync_visualizer_proxies(
    mode: Res<State<EditorMode>>,
    possession: Res<PlayPossession>,
    viewport_mode: Res<EditorViewportMode>,
    display: Res<ViewportDisplayMode>,
    settings: Res<EditorVisualizationSettings>,
    assets: Res<VisualizerProxyAssets>,
    mut commands: Commands,
    mut proxies: Query<(Entity, &EditorVisualizerProxy, &mut Transform)>,
    targets: VisualizerTargetQueries,
) {
    let mut desired = std::collections::HashSet::new();
    let collider_mode = *viewport_mode == EditorViewportMode::Collider;
    let tools_active = *mode.get() == EditorMode::Editing
        || (*mode.get() == EditorMode::Playing && *possession == PlayPossession::Ejected);
    let proxies_active = tools_active
        && !display.clean_game_view
        && settings.enabled
        && (settings.pickable_proxies || collider_mode);

    if proxies_active {
        if settings.show_colliders || collider_mode {
            desired.extend(
                targets
                    .colliders
                    .iter()
                    .map(|target| (target, VisualizerKind::Collider)),
            );
        }
        if settings.show_lights && !collider_mode {
            desired.extend(
                targets
                    .lights
                    .iter()
                    .map(|target| (target, VisualizerKind::Light)),
            );
        }
        if settings.show_spawns && !collider_mode {
            desired.extend(
                targets
                    .spawns
                    .iter()
                    .map(|target| (target, VisualizerKind::PlayerSpawn)),
            );
        }
        if settings.show_prefab_model_anchors && !collider_mode {
            desired.extend(
                targets
                    .prefabs
                    .iter()
                    .map(|target| (target, VisualizerKind::Prefab)),
            );
            desired.extend(
                targets
                    .models
                    .iter()
                    .map(|target| (target, VisualizerKind::Model)),
            );
        }
        if settings.show_runtime && !collider_mode {
            desired.extend(
                targets
                    .players
                    .iter()
                    .map(|target| (target, VisualizerKind::Player)),
            );
            desired.extend(
                targets
                    .cameras
                    .iter()
                    .map(|target| (target, VisualizerKind::Camera)),
            );
            desired.extend(
                targets
                    .project_suns
                    .iter()
                    .map(|target| (target, VisualizerKind::ProjectSun)),
            );
        }
    }

    let existing: std::collections::HashSet<(Entity, VisualizerKind)> = proxies
        .iter()
        .map(|(_, proxy, _)| (proxy.target, proxy.kind))
        .collect();

    for (entity, proxy, mut transform) in &mut proxies {
        if !desired.contains(&(proxy.target, proxy.kind))
            || targets.transforms.get(proxy.target).is_err()
        {
            commands.entity(entity).despawn();
        } else if let Ok(global) = targets.transforms.get(proxy.target) {
            transform.translation = global.translation();
        }
    }

    for (target, kind) in desired {
        let Ok(global) = targets.transforms.get(target) else {
            continue;
        };
        if existing.contains(&(target, kind)) {
            continue;
        }
        commands.spawn((
            EditorOnly,
            RaytracingExcluded,
            Name::new(format!("{kind:?} Visualizer Proxy")),
            EditorVisualizerProxy { target, kind },
            Mesh3d(assets.mesh.clone()),
            MeshMaterial3d(assets.material.clone()),
            Transform::from_translation(global.translation()),
        ));
    }
}

#[allow(clippy::too_many_arguments)]
fn draw_visualizers(
    viewport_mode: Res<EditorViewportMode>,
    settings: Res<EditorVisualizationSettings>,
    selected: Res<SelectedEntity>,
    mut gizmos: Gizmos,
    colliders: Query<(Entity, &PhysicsBody, &GlobalTransform), With<LevelObject>>,
    lights: Query<(Entity, &LightDesc, &GlobalTransform), With<LevelObject>>,
    spawns: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<PlayerSpawn>)>,
    prefabs: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<PrefabRef>)>,
    models: Query<
        (Entity, &GlobalTransform),
        (
            With<LevelObject>,
            Or<(With<ModelRef>, With<SkinnedMeshRenderer>)>,
        ),
    >,
    players: Query<(Entity, &GlobalTransform), With<Player>>,
    cameras: Query<(Entity, &GlobalTransform), With<PlayerCamera>>,
    project_suns: Query<(Entity, &GlobalTransform), With<ProjectSun>>,
    triggers: Query<(Entity, &TriggerVolume, &GlobalTransform), With<LevelObject>>,
    weapons: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<WeaponSpawn>)>,
    teams: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<TeamSpawn>)>,
    objectives: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<ObjectiveMarker>)>,
) {
    let collider_mode = *viewport_mode == EditorViewportMode::Collider;
    if !settings.enabled && !collider_mode {
        return;
    }

    if settings.show_colliders || collider_mode {
        for (entity, body, global) in &colliders {
            let color = if selected.0 == Some(entity) {
                if collider_mode {
                    Color::srgba(0.65, 1.0, 0.55, 1.0)
                } else {
                    Color::srgba(0.55, 1.0, 0.45, 0.95)
                }
            } else if collider_mode {
                Color::srgba(0.35, 1.0, 0.45, 0.9)
            } else {
                Color::srgba(0.25, 0.9, 0.35, 0.55)
            };
            draw_collider(&mut gizmos, global, &body.collider, color);
        }
    }

    if settings.show_lights && !collider_mode {
        for (entity, light, global) in &lights {
            let color = if selected.0 == Some(entity) {
                Color::srgba(1.0, 0.9, 0.35, 1.0)
            } else {
                Color::srgba(1.0, 0.78, 0.25, 0.75)
            };
            draw_light(&mut gizmos, global, light, color, true);
        }
    }

    if settings.show_spawns {
        for (entity, global) in &spawns {
            let color = if selected.0 == Some(entity) {
                Color::srgba(0.2, 1.0, 1.0, 1.0)
            } else {
                Color::srgba(0.15, 0.85, 1.0, 0.75)
            };
            draw_spawn(&mut gizmos, global, color);
        }
    }

    if settings.show_prefab_model_anchors {
        for (entity, global) in &prefabs {
            draw_anchor(
                &mut gizmos,
                global,
                selected.0 == Some(entity),
                Color::srgb(0.75, 0.45, 1.0),
            );
        }
        for (entity, global) in &models {
            draw_anchor(
                &mut gizmos,
                global,
                selected.0 == Some(entity),
                Color::srgb(0.5, 0.6, 1.0),
            );
        }
    }

    if settings.show_runtime {
        for (entity, global) in &players {
            let color = if selected.0 == Some(entity) {
                Color::srgba(0.25, 0.65, 1.0, 1.0)
            } else {
                Color::srgba(0.25, 0.65, 1.0, 0.7)
            };
            draw_player(&mut gizmos, global, color);
        }
        for (entity, global) in &cameras {
            let color = if selected.0 == Some(entity) {
                Color::srgba(0.4, 0.85, 1.0, 1.0)
            } else {
                Color::srgba(0.4, 0.85, 1.0, 0.7)
            };
            draw_camera_frustum(&mut gizmos, global, color);
        }
        for (entity, global) in &project_suns {
            let color = if selected.0 == Some(entity) {
                Color::srgba(1.0, 0.88, 0.32, 1.0)
            } else {
                Color::srgba(1.0, 0.78, 0.18, 0.75)
            };
            draw_project_sun(&mut gizmos, global, color, true);
        }
    }

    if settings.show_gameplay && !collider_mode {
        for (entity, trigger, global) in &triggers {
            draw_oriented_box(
                &mut gizmos,
                global,
                trigger.half_extents,
                if selected.0 == Some(entity) {
                    Color::srgba(1.0, 0.55, 0.95, 0.95)
                } else {
                    Color::srgba(0.9, 0.3, 0.9, 0.42)
                },
            );
        }
        for (entity, global) in &weapons {
            draw_anchor(
                &mut gizmos,
                global,
                selected.0 == Some(entity),
                Color::srgb(1.0, 0.35, 0.2),
            );
        }
        for (entity, global) in &teams {
            draw_anchor(
                &mut gizmos,
                global,
                selected.0 == Some(entity),
                Color::srgb(0.2, 0.85, 0.35),
            );
        }
        for (entity, global) in &objectives {
            draw_anchor(
                &mut gizmos,
                global,
                selected.0 == Some(entity),
                Color::srgb(0.95, 0.85, 0.2),
            );
        }
    }
}

fn draw_split_collider_visualizers(
    viewport_mode: Res<EditorViewportMode>,
    settings: Res<EditorVisualizationSettings>,
    selected: Res<SelectedEntity>,
    mut gizmos: Gizmos,
    colliders: Query<
        (Entity, &ColliderDesc, &GlobalTransform),
        (With<LevelObject>, Without<PhysicsBody>),
    >,
) {
    let collider_mode = *viewport_mode == EditorViewportMode::Collider;
    if !settings.enabled && !collider_mode {
        return;
    }
    if !(settings.show_colliders || collider_mode) {
        return;
    }
    for (entity, collider, global) in &colliders {
        let color = if selected.0 == Some(entity) {
            Color::srgba(0.65, 1.0, 0.55, 1.0)
        } else if collider_mode {
            Color::srgba(0.35, 1.0, 0.45, 0.9)
        } else {
            Color::srgba(0.25, 0.9, 0.35, 0.55)
        };
        draw_collider_desc(&mut gizmos, global, collider, color);
    }
}

fn draw_post_process_volume_gizmos(
    viewport_mode: Res<EditorViewportMode>,
    settings: Res<EditorVisualizationSettings>,
    selected: Res<SelectedEntity>,
    mut gizmos: Gizmos,
    volumes: Query<(Entity, &PostProcessVolumeDesc, &GlobalTransform), With<LevelObject>>,
) {
    if *viewport_mode == EditorViewportMode::Collider || !settings.enabled || !settings.show_volumes
    {
        return;
    }
    for (entity, volume, global) in &volumes {
        let t = ((volume.priority + 10) as f32 / 110.0).clamp(0.0, 1.0);
        let border = Color::srgba(0.2 + 0.8 * t, 0.45 + 0.2 * (1.0 - t), 1.0 - 0.65 * t, 0.95);
        draw_oriented_box(
            &mut gizmos,
            global,
            volume.half_extents,
            if selected.0 == Some(entity) {
                Color::srgba(1.0, 0.76, 0.34, 1.0)
            } else {
                border.with_alpha(0.55)
            },
        );
    }
}

fn draw_collider(
    gizmos: &mut Gizmos,
    global: &GlobalTransform,
    collider: &ColliderConstructor,
    color: Color,
) {
    match collider {
        ColliderConstructor::Cuboid {
            x_length,
            y_length,
            z_length,
        } => draw_box(
            gizmos,
            global.translation(),
            global.rotation(),
            Vec3::new(*x_length, *y_length, *z_length) * 0.5,
            color,
        ),
        ColliderConstructor::Sphere { radius } => {
            gizmos
                .sphere(
                    Isometry3d::new(global.translation(), global.rotation()),
                    *radius,
                    color,
                )
                .resolution(24);
        }
        ColliderConstructor::Capsule { radius, height } => {
            draw_capsule(
                gizmos,
                global.translation(),
                global.rotation(),
                *radius,
                *height,
                color,
            );
        }
        _ => {
            gizmos
                .sphere(
                    Isometry3d::new(global.translation(), global.rotation()),
                    0.5,
                    color,
                )
                .resolution(16);
        }
    }
}

fn draw_collider_desc(
    gizmos: &mut Gizmos,
    global: &GlobalTransform,
    collider: &ColliderDesc,
    color: Color,
) {
    if !collider.enabled {
        return;
    }
    match &collider.shape {
        ColliderShapeDesc::Cuboid {
            x_length,
            y_length,
            z_length,
        } => draw_box(
            gizmos,
            global.translation(),
            global.rotation(),
            Vec3::new(*x_length, *y_length, *z_length) * 0.5,
            color,
        ),
        ColliderShapeDesc::Sphere { radius } => {
            gizmos
                .sphere(
                    Isometry3d::new(global.translation(), global.rotation()),
                    *radius,
                    color,
                )
                .resolution(24);
        }
        ColliderShapeDesc::Capsule { radius, height } => {
            draw_capsule(
                gizmos,
                global.translation(),
                global.rotation(),
                *radius,
                *height,
                color,
            );
        }
        ColliderShapeDesc::StaticMesh { .. } => {
            gizmos
                .sphere(
                    Isometry3d::new(global.translation(), global.rotation()),
                    0.35,
                    color,
                )
                .resolution(16);
        }
    }
}

fn draw_light(
    gizmos: &mut Gizmos,
    global: &GlobalTransform,
    light: &LightDesc,
    color: Color,
    emphasize: bool,
) {
    let center = global.translation();
    let forward = global.forward().as_vec3();
    match light.kind {
        AuthoringLightKind::Point => {
            let radius = light.range.max(0.25);
            gizmos
                .sphere(Isometry3d::new(center, global.rotation()), radius, color)
                .resolution(32);
            if emphasize {
                gizmos
                    .circle(
                        Isometry3d::from_translation(center),
                        radius,
                        color.with_alpha(0.45),
                    )
                    .resolution(32);
            }
        }
        AuthoringLightKind::Spot => {
            let range = light.range.max(1.0);
            let end = center + forward * range;
            gizmos.arrow(center, end, color);
            let right = global.right().as_vec3();
            let up = global.up().as_vec3();
            let outer_deg = light.outer_angle_deg.max(light.inner_angle_deg);
            let radius = range * (outer_deg.to_radians() * 0.5).tan().max(0.15);
            let inner_radius = range * (light.inner_angle_deg.to_radians() * 0.5).tan().max(0.08);
            for offset in [right, -right, up, -up] {
                gizmos.line(center, end + offset * radius, color);
            }
            gizmos
                .circle(Isometry3d::new(end, global.rotation()), radius, color)
                .resolution(24);
            gizmos
                .circle(
                    Isometry3d::new(end, global.rotation()),
                    inner_radius,
                    color.with_alpha(0.38),
                )
                .resolution(24);
            if emphasize {
                gizmos
                    .sphere(
                        Isometry3d::new(end, global.rotation()),
                        radius * 0.12,
                        color.with_alpha(0.5),
                    )
                    .resolution(12);
            }
        }
        AuthoringLightKind::Directional => {
            draw_directional_light_arrow(gizmos, global, color);
        }
    }
}

fn draw_directional_light_arrow(gizmos: &mut Gizmos, global: &GlobalTransform, color: Color) {
    let center = global.translation();
    let forward = global.forward().as_vec3();
    let arrow_len = 5.0;
    let tail = center - forward * arrow_len;
    gizmos.arrow(tail, center, color);
    gizmos
        .sphere(Isometry3d::new(center, global.rotation()), 0.4, color)
        .resolution(18);
    let right = global.right().as_vec3();
    let up = global.up().as_vec3();
    for offset in [right, -right, up, -up] {
        gizmos.line(center, center + offset * 0.75, color.with_alpha(0.65));
    }
}

fn draw_spawn(gizmos: &mut Gizmos, global: &GlobalTransform, color: Color) {
    let center = global.translation();
    draw_capsule(
        gizmos,
        center,
        global.rotation(),
        PLAYER_RADIUS,
        PLAYER_LENGTH,
        color,
    );
    gizmos.arrow(center, center + global.forward().as_vec3() * 1.25, color);
    gizmos
        .sphere(
            Isometry3d::new(center + Vec3::Y * EYE_OFFSET_STAND, global.rotation()),
            0.08,
            color,
        )
        .resolution(10);
}

fn draw_anchor(gizmos: &mut Gizmos, global: &GlobalTransform, selected: bool, base: Color) {
    let center = global.translation();
    let color = if selected {
        Color::srgba(1.0, 0.76, 0.34, 1.0)
    } else {
        base.with_alpha(0.75)
    };
    gizmos.axes(Isometry3d::new(center, global.rotation()), 0.8);
    draw_box(gizmos, center, global.rotation(), Vec3::splat(0.35), color);
}

fn draw_player(gizmos: &mut Gizmos, global: &GlobalTransform, color: Color) {
    let center = global.translation();
    draw_capsule(
        gizmos,
        center,
        global.rotation(),
        PLAYER_RADIUS,
        PLAYER_LENGTH,
        color,
    );
    gizmos.arrow(center, center + global.forward().as_vec3() * 1.25, color);
    gizmos
        .sphere(
            Isometry3d::new(center + Vec3::Y * EYE_OFFSET_STAND, global.rotation()),
            0.08,
            color,
        )
        .resolution(10);
}

fn draw_project_sun(gizmos: &mut Gizmos, global: &GlobalTransform, color: Color, emphasize: bool) {
    let center = global.translation();
    gizmos
        .sphere(Isometry3d::new(center, global.rotation()), 0.45, color)
        .resolution(20);
    draw_directional_light_arrow(gizmos, global, color);
    if emphasize {
        gizmos
            .circle(
                Isometry3d::new(center, global.rotation()),
                1.0,
                color.with_alpha(0.35),
            )
            .resolution(28);
    } else {
        gizmos
            .circle(Isometry3d::new(center, global.rotation()), 0.75, color)
            .resolution(24);
    }
}

fn draw_camera_frustum(gizmos: &mut Gizmos, global: &GlobalTransform, color: Color) {
    let origin = global.translation();
    let forward = global.forward().as_vec3();
    let right = global.right().as_vec3();
    let up = global.up().as_vec3();
    let near_center = origin + forward * 0.55;
    let far_center = origin + forward * 2.4;
    let near = frustum_corners(near_center, right, up, 0.28, 0.17);
    let far = frustum_corners(far_center, right, up, 1.1, 0.66);
    for corner in near {
        gizmos.line(origin, corner, color.with_alpha(0.72));
    }
    gizmos.lineloop(near, color.with_alpha(0.72));
    gizmos.lineloop(far, color);
    for index in 0..4 {
        gizmos.line(near[index], far[index], color);
    }
    gizmos.arrow(origin, origin + forward * 1.15, color.with_alpha(0.8));
}

fn frustum_corners(
    center: Vec3,
    right: Vec3,
    up: Vec3,
    half_width: f32,
    half_height: f32,
) -> [Vec3; 4] {
    [
        center + right * half_width + up * half_height,
        center - right * half_width + up * half_height,
        center - right * half_width - up * half_height,
        center + right * half_width - up * half_height,
    ]
}

fn draw_box(gizmos: &mut Gizmos, center: Vec3, rotation: Quat, half: Vec3, color: Color) {
    let corners = [
        Vec3::new(-half.x, -half.y, -half.z),
        Vec3::new(half.x, -half.y, -half.z),
        Vec3::new(half.x, -half.y, half.z),
        Vec3::new(-half.x, -half.y, half.z),
        Vec3::new(-half.x, half.y, -half.z),
        Vec3::new(half.x, half.y, -half.z),
        Vec3::new(half.x, half.y, half.z),
        Vec3::new(-half.x, half.y, half.z),
    ]
    .map(|corner| center + rotation * corner);

    for (a, b) in [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 0),
        (4, 5),
        (5, 6),
        (6, 7),
        (7, 4),
        (0, 4),
        (1, 5),
        (2, 6),
        (3, 7),
    ] {
        gizmos.line(corners[a], corners[b], color);
    }
}

fn draw_capsule(
    gizmos: &mut Gizmos,
    center: Vec3,
    rotation: Quat,
    radius: f32,
    length: f32,
    color: Color,
) {
    let half = length * 0.5;
    let top = center + rotation * Vec3::Y * half;
    let bottom = center - rotation * Vec3::Y * half;

    gizmos
        .sphere(Isometry3d::new(top, rotation), radius, color)
        .resolution(16);
    gizmos
        .sphere(Isometry3d::new(bottom, rotation), radius, color)
        .resolution(16);

    for (x, z) in [(radius, 0.0), (-radius, 0.0), (0.0, radius), (0.0, -radius)] {
        let offset = rotation * Vec3::new(x, 0.0, z);
        gizmos.line(top + offset, bottom + offset, color);
    }
}

fn draw_oriented_box(
    gizmos: &mut Gizmos,
    global: &GlobalTransform,
    half_extents: Vec3,
    color: Color,
) {
    let rotation = global.rotation();
    let center = global.translation();
    let corners = box_corners(center, rotation, half_extents);
    for (a, b) in box_edges() {
        gizmos.line(corners[a], corners[b], color);
    }
}

fn box_corners(center: Vec3, rotation: Quat, half_extents: Vec3) -> [Vec3; 8] {
    let hx = half_extents.x;
    let hy = half_extents.y;
    let hz = half_extents.z;
    [
        center + rotation * Vec3::new(-hx, -hy, -hz),
        center + rotation * Vec3::new(hx, -hy, -hz),
        center + rotation * Vec3::new(hx, -hy, hz),
        center + rotation * Vec3::new(-hx, -hy, hz),
        center + rotation * Vec3::new(-hx, hy, -hz),
        center + rotation * Vec3::new(hx, hy, -hz),
        center + rotation * Vec3::new(hx, hy, hz),
        center + rotation * Vec3::new(-hx, hy, hz),
    ]
}

fn box_edges() -> [(usize, usize); 12] {
    [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 0),
        (4, 5),
        (5, 6),
        (6, 7),
        (7, 4),
        (0, 4),
        (1, 5),
        (2, 6),
        (3, 7),
    ]
}

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

    #[test]
    fn audio_radii_are_finite_positive_and_ordered() {
        assert_eq!(normalized_audio_radii(2.0, 8.0), (2.0, 8.0));
        assert_eq!(normalized_audio_radii(4.0, 1.0), (4.0, 4.0));
        assert_eq!(
            normalized_audio_radii(f32::NAN, f32::INFINITY),
            (0.05, 0.05)
        );
    }

    #[test]
    fn listener_geometry_respects_rotation_and_ear_gap() {
        let geometry = audio_listener_geometry(
            Vec3::new(2.0, 3.0, 4.0),
            Quat::from_rotation_y(std::f32::consts::FRAC_PI_2),
            0.4,
        );

        assert!((geometry.left_ear.distance(geometry.right_ear) - 0.4).abs() < 1.0e-5);
        assert!((geometry.forward_end - Vec3::new(1.15, 3.0, 4.0)).length() < 1.0e-5);
        assert!(geometry.head_radius >= 0.14);
    }
}
