use rapier::math::Real;

use crate::debug_render::DebugRenderPipelineResource;
use crate::harness::{Harness, RapierBroadPhaseType};
use crate::testbed::{
    PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, RunMode, TestbedActionFlags,
    TestbedState, TestbedStateFlags, UiTab,
};

pub use egui;

use crate::PhysicsState;
use crate::settings::SettingValue;
use egui::{ComboBox, RichText, Slider, Ui, Window};
use web_time::Instant;

#[cfg(feature = "dim3")]
use rapier::dynamics::FrictionModel;

/// Sets up a custom warm theme that complements the app's off-white background.
fn setup_custom_theme(ctx: &egui::Context) {
    use egui::{Color32, CornerRadius, Stroke};

    let bg_fill = Color32::from_rgb(250, 250, 245);
    let window_fill = Color32::from_rgb(252, 252, 248);
    let faint_bg = Color32::from_rgb(240, 240, 232);
    let extreme_bg = Color32::from_rgb(255, 255, 252);

    let text_color = Color32::from_rgb(60, 58, 52);

    let accent = Color32::from_rgb(82, 130, 150);
    let accent_active = Color32::from_rgb(70, 115, 135);

    let widget_bg = Color32::from_rgb(235, 235, 225);
    let widget_bg_hover = Color32::from_rgb(225, 225, 215);
    let widget_bg_active = Color32::from_rgb(215, 215, 205);

    let stroke_color = Color32::from_rgb(200, 198, 190);
    let stroke_hover = Color32::from_rgb(180, 178, 170);

    let rounding = CornerRadius::same(6);
    let small_rounding = CornerRadius::same(4);

    ctx.global_style_mut(|style| {
        let v = &mut style.visuals;
        v.dark_mode = false;

        v.widgets.noninteractive.bg_fill = faint_bg;
        v.widgets.noninteractive.weak_bg_fill = faint_bg;
        v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, stroke_color);
        v.widgets.noninteractive.corner_radius = rounding;
        v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, text_color);

        v.widgets.inactive.bg_fill = widget_bg;
        v.widgets.inactive.weak_bg_fill = widget_bg;
        v.widgets.inactive.bg_stroke = Stroke::new(1.0, stroke_color);
        v.widgets.inactive.corner_radius = small_rounding;
        v.widgets.inactive.fg_stroke = Stroke::new(1.0, text_color);

        v.widgets.hovered.bg_fill = widget_bg_hover;
        v.widgets.hovered.weak_bg_fill = widget_bg_hover;
        v.widgets.hovered.bg_stroke = Stroke::new(1.0, stroke_hover);
        v.widgets.hovered.corner_radius = small_rounding;
        v.widgets.hovered.fg_stroke = Stroke::new(1.5, text_color);

        v.widgets.active.bg_fill = widget_bg_active;
        v.widgets.active.weak_bg_fill = widget_bg_active;
        v.widgets.active.bg_stroke = Stroke::new(1.0, accent);
        v.widgets.active.corner_radius = small_rounding;
        v.widgets.active.fg_stroke = Stroke::new(2.0, accent_active);

        v.widgets.open.bg_fill = widget_bg;
        v.widgets.open.weak_bg_fill = widget_bg;
        v.widgets.open.bg_stroke = Stroke::new(1.0, stroke_color);
        v.widgets.open.corner_radius = small_rounding;
        v.widgets.open.fg_stroke = Stroke::new(1.0, text_color);

        v.selection.bg_fill = accent.gamma_multiply(0.25);
        v.selection.stroke = Stroke::new(1.0, accent);

        v.hyperlink_color = accent;
        v.faint_bg_color = faint_bg;
        v.extreme_bg_color = extreme_bg;
        v.code_bg_color = Color32::from_rgb(230, 230, 220);
        v.warn_fg_color = Color32::from_rgb(180, 120, 60);
        v.error_fg_color = Color32::from_rgb(180, 70, 70);

        v.window_corner_radius = CornerRadius::same(8);
        v.window_fill = window_fill;
        v.window_stroke = Stroke::new(1.0, stroke_color);

        v.panel_fill = bg_fill;

        v.slider_trailing_fill = true;
        v.handle_shape = egui::style::HandleShape::Circle;

        style.spacing.item_spacing = egui::vec2(6.0, 3.0);
        style.spacing.window_margin = egui::Margin::same(10);
        style.spacing.button_padding = egui::vec2(6.0, 3.0);
        style.spacing.slider_width = 130.0;
        style.spacing.indent = 14.0;
        style.spacing.interact_size = egui::vec2(32.0, 18.0);
        style.spacing.combo_width = 100.0;
    });
}

pub(crate) fn update_ui(
    ui_context: &egui::Context,
    state: &mut TestbedState,
    harness: &mut Harness,
    debug_render: &mut DebugRenderPipelineResource,
) {
    setup_custom_theme(ui_context);

    #[cfg(feature = "profiler_ui")]
    {
        profiler_ui(ui_context);
    }

    example_settings_ui(ui_context, state);

    Window::new("Rapier Testbed")
        .default_width(300.0)
        .show(ui_context, |ui| {
            // ═══════════════════════════════════════════════════════════════
            // TAB BAR
            // ═══════════════════════════════════════════════════════════════
            ui.horizontal(|ui| {
                ui.selectable_value(&mut state.selected_tab, UiTab::Examples, "Examples");
                ui.selectable_value(&mut state.selected_tab, UiTab::Settings, "Settings");
                ui.selectable_value(&mut state.selected_tab, UiTab::Performance, "Performance");
            });

            ui.separator();

            // ═══════════════════════════════════════════════════════════════
            // TAB CONTENT
            // ═══════════════════════════════════════════════════════════════
            egui::ScrollArea::vertical()
                .max_height(400.0)
                .show(ui, |ui| match state.selected_tab {
                    UiTab::Examples => {
                        examples_tab(ui, state);
                    }
                    UiTab::Settings => {
                        settings_tab(ui, state, harness, debug_render);
                    }
                    UiTab::Performance => {
                        performance_tab(ui, harness);
                    }
                });

            ui.separator();

            // ═══════════════════════════════════════════════════════════════
            // BOTTOM CONTROLS - Always visible
            // ═══════════════════════════════════════════════════════════════
            ui.horizontal(|ui| {
                // Play/Pause
                let (label, hover) = if state.running == RunMode::Stop {
                    ("Play", "Start simulation (T)")
                } else {
                    ("Pause", "Pause simulation (T)")
                };

                if ui.button(label).on_hover_text(hover).clicked() {
                    state.running = if state.running == RunMode::Stop {
                        RunMode::Running
                    } else {
                        RunMode::Stop
                    };
                }

                // Step
                if ui.button("Step").on_hover_text("Single step (S)").clicked() {
                    state.running = RunMode::Step;
                }

                // Restart
                if ui
                    .button("Restart")
                    .on_hover_text("Restart example (R)")
                    .clicked()
                {
                    state.action_flags.set(TestbedActionFlags::RESTART, true);
                }

                ui.separator();

                // Save/Restore
                if ui.button("Save").on_hover_text("Save snapshot").clicked() {
                    state
                        .action_flags
                        .set(TestbedActionFlags::TAKE_SNAPSHOT, true);
                }

                if ui
                    .button("Restore")
                    .on_hover_text("Restore snapshot")
                    .clicked()
                {
                    state
                        .action_flags
                        .set(TestbedActionFlags::RESTORE_SNAPSHOT, true);
                }

                ui.separator();

                if ui
                    .button("Frame all")
                    .on_hover_text("Recenter the camera so the entire scene fills the view")
                    .clicked()
                {
                    state
                        .action_flags
                        .set(TestbedActionFlags::FRAME_SCENE, true);
                }
            });
        });
}

fn examples_tab(ui: &mut Ui, state: &mut TestbedState) {
    // Backend selector (if multiple backends available)
    if state.backend_names.len() > 1 {
        ui.horizontal(|ui| {
            ui.label("Backend:");
            let mut backend_changed = false;
            ComboBox::from_id_salt("backend_combo")
                .width(150.0)
                .selected_text(state.backend_names[state.selected_backend])
                .show_ui(ui, |ui| {
                    for (id, name) in state.backend_names.iter().enumerate() {
                        backend_changed = ui
                            .selectable_value(&mut state.selected_backend, id, *name)
                            .changed()
                            || backend_changed;
                    }
                });

            if backend_changed {
                state
                    .action_flags
                    .set(TestbedActionFlags::BACKEND_CHANGED, true);
            }
        });
    }

    // Navigation and backend selector row
    ui.horizontal(|ui| {
        // Previous/Next buttons (navigate in display order)
        if ui
            .add_enabled(state.selected_display_index > 0, egui::Button::new("<"))
            .on_hover_text("Previous example")
            .clicked()
        {
            state.selected_display_index -= 1;
            state
                .action_flags
                .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
        }

        if ui
            .add_enabled(
                state.selected_display_index + 1 < state.examples.len(),
                egui::Button::new(">"),
            )
            .on_hover_text("Next example")
            .clicked()
        {
            state.selected_display_index += 1;
            state
                .action_flags
                .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
        }

        // Current example name
        if let Some(current) = state.examples.get(state.selected_display_index) {
            ui.label(
                RichText::new(format!("[{}] {}", current.group, current.name))
                    .strong()
                    .italics(),
            );
        }
    });

    ui.add_space(4.0);
    ui.separator();
    ui.add_space(4.0);

    // Display examples grouped by their assigned groups
    for group in &state.example_groups.clone() {
        // Collect examples in this group with their display indices
        let examples_in_group: Vec<(usize, &str)> = state
            .examples
            .iter()
            .enumerate()
            .filter(|(_, e)| e.group == *group)
            .map(|(display_idx, e)| (display_idx, e.name))
            .collect();

        if examples_in_group.is_empty() {
            continue;
        }

        egui::CollapsingHeader::new(format!("{} ({})", group, examples_in_group.len()))
            .default_open(false)
            .show(ui, |ui| {
                for (display_idx, name) in examples_in_group {
                    let is_selected = state.selected_display_index == display_idx;
                    let text = if is_selected {
                        RichText::new(name).strong()
                    } else {
                        RichText::new(name)
                    };

                    if ui
                        .selectable_label(is_selected, text)
                        .on_hover_text("Click to run this example")
                        .clicked()
                        && !is_selected
                    {
                        state.selected_display_index = display_idx;
                        state
                            .action_flags
                            .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
                    }
                }
            });
    }
}

fn settings_tab(
    ui: &mut Ui,
    state: &mut TestbedState,
    harness: &mut Harness,
    debug_render: &mut DebugRenderPipelineResource,
) {
    let integration_parameters = &mut harness.physics.integration_parameters;
    let is_physx = state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
        || state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR;

    // ─────────────────────────────────────────────────────────────────
    // RENDERING
    // ─────────────────────────────────────────────────────────────────
    ui.label(RichText::new("Rendering").strong());
    ui.add_space(2.0);

    let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES);
    if ui
        .checkbox(&mut draw_surfaces, "Surfaces")
        .on_hover_text("Render collider surfaces.")
        .changed()
    {
        state
            .flags
            .set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces);
    }

    ui.checkbox(&mut debug_render.enabled, "Debug render")
        .on_hover_text("Show debug wireframes and contacts.");

    // ─────────────────────────────────────────────────────────────────
    // SIMULATION
    // ─────────────────────────────────────────────────────────────────
    ui.label(RichText::new("Simulation").strong());
    ui.add_space(2.0);

    // Frequency
    let mut frequency = integration_parameters.inv_dt().round() as u32;
    if ui
        .add(
            Slider::new(&mut frequency, 30..=240)
                .text("Hz")
                .clamping(egui::SliderClamping::Never),
        )
        .on_hover_text("Simulation frequency. Higher = more accurate but slower.")
        .changed()
    {
        integration_parameters.set_inv_dt(frequency as Real);
    }

    // Steps per frame
    ui.add(Slider::new(&mut state.nsteps, 1..=100).text("Steps/frame"))
        .on_hover_text("Physics steps per rendered frame.");

    // Gravity slider — operates along the current up-axis so the
    // slider's "down" matches the camera's "down". Scenes that switch
    // to Z-up (e.g. the MJCF demo) get gravity along -Z instead of the
    // hard-coded -Y. Any orthogonal components of gravity are
    // preserved when the slider moves.
    let up = state.up_axis;
    let mut gravity_along_up = harness.physics.gravity.dot(up);
    if ui
        .add(Slider::new(&mut gravity_along_up, 0.0..=-200.0).text("Gravity"))
        .on_hover_text("Gravity (m/s^2) along the up-axis. Default: -9.81")
        .changed()
    {
        let current = harness.physics.gravity.dot(up);
        harness.physics.gravity += up * (gravity_along_up - current);
    }

    // Sleep
    let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP);
    if ui
        .checkbox(&mut sleep, "Sleep enabled")
        .on_hover_text("Allow resting bodies to sleep for better performance.")
        .changed()
    {
        state.flags.set(TestbedStateFlags::SLEEP, sleep);
    }

    ui.add_space(8.0);

    // ─────────────────────────────────────────────────────────────────
    // SOLVER
    // ─────────────────────────────────────────────────────────────────
    ui.label(RichText::new("Solver").strong());
    ui.add_space(2.0);

    ui.add(Slider::new(&mut integration_parameters.num_solver_iterations, 1..=10).text("Substeps"))
        .on_hover_text("Main solver iterations. Higher = more stable stacking.");

    if !is_physx {
        ui.add(
            Slider::new(
                &mut integration_parameters.num_internal_pgs_iterations,
                1..=40,
            )
            .text("PGS iters"),
        )
        .on_hover_text("Internal Projected Gauss-Seidel iterations.");

        ui.add(
            Slider::new(
                &mut integration_parameters.num_internal_stabilization_iterations,
                0..=100,
            )
            .text("Relaxation"),
        )
        .on_hover_text("Position stabilization iterations.");

        ui.add(
            Slider::new(&mut integration_parameters.warmstart_coefficient, 0.0..=1.0)
                .text("Warmstart"),
        )
        .on_hover_text("Reuse previous impulses for faster convergence.");
    }

    ui.add_space(8.0);

    // ─────────────────────────────────────────────────────────────────
    // CONTACTS (Rapier only)
    // ─────────────────────────────────────────────────────────────────
    if !is_physx {
        ui.label(RichText::new("Contacts").strong());
        ui.add_space(2.0);

        let mut substep_params = *integration_parameters;
        substep_params.dt /= substep_params.num_solver_iterations as Real;
        let curr_erp = substep_params.contact_softness.erp(substep_params.dt);
        let curr_cfm = substep_params
            .contact_softness
            .cfm_factor(substep_params.dt);

        ui.add(
            Slider::new(
                &mut integration_parameters.contact_softness.natural_frequency,
                0.01..=120.0,
            )
            .text("Frequency"),
        )
        .on_hover_text(format!(
            "Contact stiffness (Hz). Higher = stiffer.\nERP = {curr_erp:.3}"
        ));

        ui.add(
            Slider::new(
                &mut integration_parameters.contact_softness.damping_ratio,
                0.01..=20.0,
            )
            .text("Damping"),
        )
        .on_hover_text(format!(
            "Contact damping. 1.0 = critical.\nCFM = {curr_cfm:.5}"
        ));

        #[cfg(feature = "dim3")]
        {
            ui.horizontal(|ui| {
                ui.label("Friction model:");
                egui::ComboBox::from_id_salt("friction_model")
                    .width(100.0)
                    .selected_text(format!("{:?}", integration_parameters.friction_model))
                    .show_ui(ui, |ui| {
                        for model in [FrictionModel::Simplified, FrictionModel::Coulomb] {
                            ui.selectable_value(
                                &mut integration_parameters.friction_model,
                                model,
                                format!("{model:?}"),
                            )
                            .on_hover_text(match model {
                                FrictionModel::Simplified => "Fast friction approximation",
                                FrictionModel::Coulomb => "Accurate Coulomb friction",
                            });
                        }
                    });
            });
        }

        ui.add_space(8.0);
    }

    // ─────────────────────────────────────────────────────────────────
    // ADVANCED (Rapier only)
    // ─────────────────────────────────────────────────────────────────
    if !is_physx {
        ui.label(RichText::new("Advanced").strong());
        ui.add_space(2.0);

        // Broad-phase
        ui.horizontal(|ui| {
            ui.label("Broad-phase:");
            let mut bp_changed = false;
            egui::ComboBox::from_id_salt("broad_phase")
                .width(120.0)
                .selected_text(match state.broad_phase_type {
                    RapierBroadPhaseType::BvhSubtreeOptimizer => "BVH (optimized)",
                    RapierBroadPhaseType::BvhWithoutOptimization => "BVH (basic)",
                })
                .show_ui(ui, |ui| {
                    for (bpt, label) in [
                        (RapierBroadPhaseType::BvhSubtreeOptimizer, "BVH (optimized)"),
                        (RapierBroadPhaseType::BvhWithoutOptimization, "BVH (basic)"),
                    ] {
                        bp_changed = ui
                            .selectable_value(&mut state.broad_phase_type, bpt, label)
                            .changed()
                            || bp_changed;
                    }
                });

            if bp_changed {
                harness.physics.broad_phase = state.broad_phase_type.init_broad_phase();
                state.action_flags.set(TestbedActionFlags::RESTART, true);
            }
        });

        ui.add(
            Slider::new(&mut integration_parameters.max_ccd_substeps, 0..=10).text("CCD substeps"),
        )
        .on_hover_text("Continuous collision detection substeps.");

        ui.add(
            Slider::new(&mut integration_parameters.min_island_size, 1..=10_000)
                .text("Min island sz."),
        )
        .on_hover_text("Minimum bodies per simulation island.");

        #[cfg(feature = "parallel")]
        {
            let mut num_threads = harness.state.num_threads();
            if ui
                .add(Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("Threads"))
                .on_hover_text("Parallel solver threads.")
                .changed()
            {
                harness.state.set_num_threads(num_threads);
            }
        }

        ui.add_space(8.0);
    }
}

fn performance_tab(ui: &mut Ui, harness: &Harness) {
    // ─────────────────────────────────────────────────────────────────
    // SCENE INFO
    // ─────────────────────────────────────────────────────────────────
    ui.label(RichText::new("Scene").strong());
    ui.add_space(2.0);

    egui::Grid::new("scene_grid")
        .num_columns(2)
        .spacing([20.0, 2.0])
        .show(ui, |ui| {
            ui.label("Bodies:");
            ui.label(format!("{}", harness.physics.bodies.len()));
            ui.end_row();

            ui.label("Colliders:");
            ui.label(format!("{}", harness.physics.colliders.len()));
            ui.end_row();

            ui.label("Joints:");
            ui.label(format!("{}", harness.physics.impulse_joints.len()));
            ui.end_row();
        });

    ui.add_space(8.0);
    ui.separator();
    ui.add_space(4.0);

    // ─────────────────────────────────────────────────────────────────
    // TIMING - Full details
    // ─────────────────────────────────────────────────────────────────
    let counters = &harness.physics.pipeline.counters;
    let total_ms = counters.step_time_ms();
    let fps = if total_ms > 0.0 {
        (1000.0 / total_ms).round()
    } else {
        0.0
    };

    ui.label(RichText::new(format!("Total: {:.2}ms - {:.0} FPS", total_ms, fps)).strong());
    ui.add_space(4.0);

    // Collision detection
    egui::CollapsingHeader::new(format!(
        "Collision detection: {:.2}ms",
        counters.collision_detection_time_ms()
    ))
    .id_salt("collision_detection")
    .default_open(false)
    .show(ui, |ui| {
        ui.label(format!(
            "Broad-phase: {:.2}ms",
            counters.broad_phase_time_ms()
        ));
        ui.label(format!(
            "Final broad-phase: {:.2}ms",
            counters.cd.final_broad_phase_time.time_ms()
        ));
        ui.label(format!(
            "Narrow-phase: {:.2}ms",
            counters.narrow_phase_time_ms()
        ));
    });

    // Solver
    egui::CollapsingHeader::new(format!("Solver: {:.2}ms", counters.solver_time_ms()))
        .id_salt("solver")
        .default_open(false)
        .show(ui, |ui| {
            ui.label(format!(
                "Velocity assembly: {:.2}ms",
                counters.solver.velocity_assembly_time.time_ms()
            ));
            ui.label(format!(
                "  > Solver bodies: {:.2}ms",
                counters
                    .solver
                    .velocity_assembly_time_solver_bodies
                    .time_ms()
            ));
            ui.label(format!(
                "  > Constraints init: {:.2}ms",
                counters
                    .solver
                    .velocity_assembly_time_constraints_init
                    .time_ms()
            ));
            ui.label(format!(
                "Velocity resolution: {:.2}ms",
                counters.velocity_resolution_time_ms()
            ));
            ui.label(format!(
                "Velocity integration: {:.2}ms",
                counters.solver.velocity_update_time.time_ms()
            ));
            ui.label(format!(
                "Writeback: {:.2}ms",
                counters.solver.velocity_writeback_time.time_ms()
            ));
        });

    // CCD
    egui::CollapsingHeader::new(format!("CCD: {:.2}ms", counters.ccd_time_ms()))
        .id_salt("ccd")
        .default_open(false)
        .show(ui, |ui| {
            ui.label(format!("# of substeps: {}", counters.ccd.num_substeps));
            ui.label(format!(
                "TOI computation: {:.2}ms",
                counters.ccd.toi_computation_time.time_ms()
            ));
            ui.label(format!(
                "Broad-phase: {:.2}ms",
                counters.ccd.broad_phase_time.time_ms()
            ));
            ui.label(format!(
                "Narrow-phase: {:.2}ms",
                counters.ccd.narrow_phase_time.time_ms()
            ));
            ui.label(format!(
                "Solver: {:.2}ms",
                counters.ccd.solver_time.time_ms()
            ));
        });

    // Other timings
    ui.add_space(4.0);
    ui.label(format!(
        "Island computation: {:.2}ms",
        counters.island_construction_time_ms()
    ));
    ui.label(format!(
        "Active constraints collection: {:.2}ms",
        counters.stages.island_constraints_collection_time.time_ms()
    ));
    ui.label(format!(
        "Mass properties update: {:.2}ms",
        counters.update_time_ms()
    ));
    ui.label(format!(
        "User changes: {:.2}ms",
        counters.stages.user_changes.time_ms()
    ));
    ui.label(format!("Debug timer: {:.2}ms", counters.custom.time_ms()));

    ui.add_space(8.0);
    ui.separator();
    ui.add_space(4.0);

    // ─────────────────────────────────────────────────────────────────
    // SERIALIZATION INFO
    // ─────────────────────────────────────────────────────────────────
    egui::CollapsingHeader::new("Serialization Hashes")
        .default_open(false)
        .show(ui, |ui| {
            ui.label(
                egui::RichText::new(serialization_string(
                    harness.state.timestep_id,
                    &harness.physics,
                ))
                .small()
                .monospace(),
            );
        });
}

fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String {
    let t = Instant::now();
    let bf = bincode::serialize(&physics.broad_phase).unwrap();
    let nf = bincode::serialize(&physics.narrow_phase).unwrap();
    let bs = bincode::serialize(&physics.bodies).unwrap();
    let cs = bincode::serialize(&physics.colliders).unwrap();
    let js = bincode::serialize(&physics.impulse_joints).unwrap();
    let serialization_time = Instant::now() - t;
    let hash_bf = md5::compute(&bf);
    let hash_nf = md5::compute(&nf);
    let hash_bodies = md5::compute(&bs);
    let hash_colliders = md5::compute(&cs);
    let hash_joints = md5::compute(&js);
    format!(
        "Frame: {} ({:.1}ms)\n\
         Broad:     {:.1}KB {}\n\
         Narrow:    {:.1}KB {}\n\
         Bodies:    {:.1}KB {}\n\
         Colliders: {:.1}KB {}\n\
         Joints:    {:.1}KB {}",
        timestep_id,
        serialization_time.as_secs_f64() * 1000.0,
        bf.len() as f32 / 1000.0,
        format!("{hash_bf:?}").split_at(8).0,
        nf.len() as f32 / 1000.0,
        format!("{hash_nf:?}").split_at(8).0,
        bs.len() as f32 / 1000.0,
        format!("{hash_bodies:?}").split_at(8).0,
        cs.len() as f32 / 1000.0,
        format!("{hash_colliders:?}").split_at(8).0,
        js.len() as f32 / 1000.0,
        format!("{hash_joints:?}").split_at(8).0,
    )
}

fn example_settings_ui(ui_context: &egui::Context, state: &mut TestbedState) {
    if state.example_settings.is_empty() {
        return;
    }

    Window::new("Example Settings")
        .default_width(250.0)
        .show(ui_context, |ui| {
            let mut any_changed = false;

            for (name, value) in state.example_settings.iter_mut() {
                let prev_value = value.clone();
                match value {
                    SettingValue::Label(text) => {
                        ui.horizontal(|ui| {
                            ui.label(RichText::new(format!("{name}:")).strong());
                            ui.label(text.as_str());
                        });
                    }
                    SettingValue::F32 { value, range } => {
                        ui.add(Slider::new(value, range.clone()).text(name));
                    }
                    SettingValue::U32 { value, range } => {
                        ui.horizontal(|ui| {
                            if ui.small_button("-").on_hover_text("Decrease").clicked()
                                && *value > *range.start()
                            {
                                *value -= 1;
                            }
                            if ui.small_button("+").on_hover_text("Increase").clicked()
                                && *value < *range.end()
                            {
                                *value += 1;
                            }
                            ui.add(Slider::new(value, range.clone()).text(name));
                        });
                    }
                    SettingValue::Bool { value } => {
                        ui.checkbox(value, name);
                    }
                    SettingValue::String {
                        value,
                        range,
                        display_mode,
                    } => match display_mode {
                        crate::settings::StringDisplayMode::ComboBox => {
                            ComboBox::from_label(name)
                                .width(150.0)
                                .selected_text(&range[*value])
                                .show_ui(ui, |ui| {
                                    for (id, option) in range.iter().enumerate() {
                                        ui.selectable_value(value, id, option);
                                    }
                                });
                        }
                        crate::settings::StringDisplayMode::List => {
                            let current = range.get(*value).cloned().unwrap_or_default();
                            let n = range.len();
                            ui.horizontal(|ui| {
                                // `<` / `>` step through the list one
                                // entry at a time. Mirrors the `-` / `+`
                                // pair on `U32` settings.
                                let can_prev = n > 0 && *value > 0;
                                let can_next = n > 0 && *value + 1 < n;
                                if ui
                                    .add_enabled(can_prev, egui::Button::new("<"))
                                    .on_hover_text("Previous")
                                    .clicked()
                                {
                                    *value -= 1;
                                }
                                if ui
                                    .add_enabled(can_next, egui::Button::new(">"))
                                    .on_hover_text("Next")
                                    .clicked()
                                {
                                    *value += 1;
                                }
                                ui.label(RichText::new(format!("{name}:")).strong());
                                ui.label(current);
                            });
                            // Bound the height so a long option list
                            // doesn't crowd out the rest of the panel.
                            egui::ScrollArea::vertical()
                                .id_salt(name.as_str())
                                .max_height(300.0)
                                .auto_shrink([false, true])
                                .show(ui, |ui| {
                                    for (id, opt) in range.iter().enumerate() {
                                        let is_selected = *value == id;
                                        let text = if is_selected {
                                            RichText::new(opt).strong()
                                        } else {
                                            RichText::new(opt)
                                        };
                                        if ui.selectable_label(is_selected, text).clicked()
                                            && !is_selected
                                        {
                                            *value = id;
                                        }
                                    }
                                });
                        }
                    },
                }

                any_changed = any_changed || *value != prev_value;
            }

            if any_changed {
                state.action_flags.set(TestbedActionFlags::RESTART, true);
            }
        });
}

#[cfg(feature = "profiler_ui")]
fn profiler_ui(_ui_context: &egui::Context) {
    #[cfg(feature = "unstable-puffin-pr-235")]
    {
        let window = egui::Window::new("Profiler");
        let window = window.default_open(false);
        use std::sync::Once;
        static START: Once = Once::new();

        fn set_default_rapier_filter() {
            let mut profile_ui = puffin_egui::PROFILE_UI.lock();
            profile_ui
                .profiler_ui
                .flamegraph_options
                .scope_name_filter
                .set_filter("Harness::step_with_graphics".to_string());
        }
        START.call_once(|| {
            set_default_rapier_filter();
        });
        window.show(_ui_context, |ui| {
            if ui.button("Rapier filter").clicked() {
                set_default_rapier_filter();
            }
            puffin_egui::profiler_ui(ui);
        });
    }
}
