//! Animation controller authoring and transient viewport preview controls.

use std::collections::HashMap;

use bevy::prelude::*;
use bevy_egui::egui;
use egui_phosphor_icons::icons;
use shared::{
    AnimationClipRecord, AnimationControllerDesc, AnimationManifest, AnimationSkeletonRecord,
    AnimationStateDesc, EditorAssetRef, SkinnedMeshRenderer, COMPONENT_ANIMATION_CONTROLLER_DESC,
};

use crate::assets::animation::load_animation_manifest;
use crate::assets::asset_db::{find_asset_by_id, find_asset_by_path, AssetRegistry};
use crate::history::set_animation_controller_with_history;
use crate::ui::inspector::{
    apply_component_card_response, component_card, component_card_context, property_row,
    text_field_width, ComponentCardOptions,
};
use crate::ui::theme::{ERROR, SUCCESS, TEXT_DIM, TEXT_MUTED, WARNING};
use crate::ui::widgets::icon_button_small;

#[derive(Resource, Default)]
pub(crate) struct AnimationInspectorState {
    selected_state: HashMap<Entity, String>,
    scrub_seconds: HashMap<Entity, f32>,
}

#[derive(Debug, Clone)]
enum PreviewAction {
    Set(game::animation::AnimationPreviewRequest),
    Stop,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClipCompatibility {
    Compatible,
    Unknown,
    Mismatch,
}

impl ClipCompatibility {
    fn label(self) -> &'static str {
        match self {
            Self::Compatible => "Compatible",
            Self::Unknown => "Compatibility unknown",
            Self::Mismatch => "Skeleton mismatch",
        }
    }

    fn color(self) -> egui::Color32 {
        match self {
            Self::Compatible => SUCCESS,
            Self::Unknown => WARNING,
            Self::Mismatch => ERROR,
        }
    }
}

pub(crate) fn default_controller_for_actor(
    world: &World,
    entity: Entity,
) -> AnimationControllerDesc {
    manifest_for_actor(world, entity)
        .map(|manifest| controller_from_manifest(&manifest))
        .unwrap_or_default()
}

pub(crate) fn stop_preview_if_actor(world: &mut World, entity: Entity) {
    let previewing_actor = world
        .get_resource::<game::animation::AnimationPreviewRuntime>()
        .and_then(|runtime| runtime.request.as_ref())
        .is_some_and(|request| request.actor == entity);
    if previewing_actor {
        game::animation::stop_animation_preview(world);
    }
    if let Some(mut state) = world.get_resource_mut::<AnimationInspectorState>() {
        state.selected_state.remove(&entity);
        state.scrub_seconds.remove(&entity);
    }
}

pub(crate) fn animation_controller_inspector_ui(
    world: &mut World,
    ui: &mut egui::Ui,
    entity: Entity,
) {
    let Some(mut controller) = world.get::<AnimationControllerDesc>(entity).cloned() else {
        return;
    };
    let original = controller.clone();
    let manifest_result = manifest_for_actor(world, entity);
    let manifest = manifest_result.as_ref().ok();
    let referenced_clip_manifests = referenced_clip_manifests(world, &controller, manifest);
    let active_preview = world
        .get_resource::<game::animation::AnimationPreviewRuntime>()
        .and_then(|runtime| runtime.request.clone());
    let stored_selection = world
        .get_resource::<AnimationInspectorState>()
        .and_then(|state| state.selected_state.get(&entity).cloned());
    let stored_scrub = world
        .get_resource::<AnimationInspectorState>()
        .and_then(|state| state.scrub_seconds.get(&entity).copied());
    let mut selected_state = active_preview
        .as_ref()
        .filter(|request| request.actor == entity)
        .map(|request| request.state_id.clone())
        .or(stored_selection)
        .filter(|id| controller.state(id).is_some())
        .or_else(|| {
            controller
                .state(&controller.default_state)
                .map(|state| state.id.clone())
        })
        .or_else(|| controller.states.first().map(|state| state.id.clone()))
        .unwrap_or_default();
    let mut scrub_seconds = active_preview
        .as_ref()
        .filter(|request| request.actor == entity)
        .and_then(|request| request.seek_seconds)
        .or(stored_scrub)
        .unwrap_or_else(|| {
            controller
                .state(&selected_state)
                .map(|state| state.range.start_seconds)
                .unwrap_or(0.0)
        });
    let mut preview_action = None;

    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_ANIMATION_CONTROLLER_DESC,
            "Animation Controller",
            icons::FILM_SLATE,
        ),
    );
    let card_response = component_card(ui, &card, |ui| {
        manifest_summary(ui, &manifest_result);

        property_row(ui, "Skeleton", |ui| {
            if let Some(manifest) = manifest {
                let selected = controller
                    .skeleton
                    .as_ref()
                    .map(|skeleton| skeleton.label.as_str())
                    .filter(|label| !label.trim().is_empty())
                    .unwrap_or("None");
                egui::ComboBox::from_id_salt(("animation_skeleton", entity))
                    .selected_text(selected)
                    .width(text_field_width(ui))
                    .show_ui(ui, |ui| {
                        ui.selectable_value(&mut controller.skeleton, None, "None");
                        for skeleton in &manifest.skeletons {
                            let reference = skeleton_ref(manifest, skeleton);
                            ui.selectable_value(
                                &mut controller.skeleton,
                                Some(reference),
                                &skeleton.label,
                            );
                        }
                    });
            } else {
                ui.add_enabled(false, egui::Label::new("Manifest unavailable"));
            }
        });

        property_row(ui, "Crossfade", |ui| {
            ui.add(
                egui::DragValue::new(&mut controller.default_crossfade_seconds)
                    .range(0.0..=5.0)
                    .speed(0.01)
                    .suffix(" s"),
            );
        });

        ui.add_space(4.0);
        ui.horizontal(|ui| {
            ui.strong("States");
            ui.label(
                egui::RichText::new(controller.states.len().to_string())
                    .small()
                    .color(TEXT_MUTED),
            );
        });
        ui.separator();

        let selected_skeleton = controller.skeleton.clone();
        let mut state_id_counts = HashMap::new();
        for state in &controller.states {
            *state_id_counts.entry(state.id.clone()).or_insert(0_usize) += 1;
        }
        let mut default_state = controller.default_state.clone();
        let mut remove_state = None;
        for (index, state) in controller.states.iter_mut().enumerate() {
            ui.push_id(("animation_state", entity, index), |ui| {
                let heading = if state.label.trim().is_empty() {
                    if state.id.trim().is_empty() {
                        "Untitled State"
                    } else {
                        state.id.as_str()
                    }
                } else {
                    state.label.as_str()
                };
                ui.horizontal(|ui| {
                    ui.label(egui::RichText::new(heading).strong());
                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                        if icon_button_small(ui, icons::TRASH, "Remove state").clicked() {
                            remove_state = Some(index);
                        }
                        if default_state == state.id && !state.id.trim().is_empty() {
                            ui.label(egui::RichText::new("DEFAULT").small().color(SUCCESS));
                        }
                    });
                });

                property_row(ui, "Name", |ui| {
                    ui.add_sized(
                        [text_field_width(ui), 20.0],
                        egui::TextEdit::singleline(&mut state.label),
                    );
                });
                let previous_id = state.id.clone();
                property_row(ui, "State ID", |ui| {
                    ui.add_sized(
                        [text_field_width(ui), 20.0],
                        egui::TextEdit::singleline(&mut state.id),
                    );
                });
                if default_state == previous_id && state.id != previous_id {
                    default_state.clone_from(&state.id);
                }
                if state.id.trim().is_empty() {
                    ui.label(
                        egui::RichText::new("State ID is required")
                            .small()
                            .color(ERROR),
                    );
                } else if state_id_counts.get(&previous_id).copied().unwrap_or(0) > 1 {
                    ui.label(
                        egui::RichText::new("State ID must be unique")
                            .small()
                            .color(ERROR),
                    );
                }

                property_row(ui, "Clip", |ui| {
                    if let Some(manifest) = manifest {
                        let selected = if state.clip.label.trim().is_empty() {
                            "None"
                        } else {
                            state.clip.label.as_str()
                        };
                        egui::ComboBox::from_id_salt("clip")
                            .selected_text(selected)
                            .width(text_field_width(ui))
                            .show_ui(ui, |ui| {
                                for clip in &manifest.clips {
                                    let reference = clip_ref(manifest, clip);
                                    let suffix = format!("{:.2} s", clip.duration_seconds);
                                    ui.selectable_value(
                                        &mut state.clip,
                                        reference,
                                        format!("{}  {}", clip.label, suffix),
                                    );
                                }
                            });
                    } else {
                        ui.add_enabled(false, egui::Label::new(&state.clip.label));
                    }
                });

                if let Some((primary, clip)) = manifest.and_then(|primary| {
                    resolved_clip_for_ref(primary, &referenced_clip_manifests, &state.clip)
                        .map(|clip| (primary, clip))
                }) {
                    let compatibility =
                        clip_compatibility(primary, selected_skeleton.as_ref(), clip);
                    ui.label(
                        egui::RichText::new(compatibility.label())
                            .small()
                            .color(compatibility.color()),
                    );
                }

                property_row(ui, "Playback", |ui| {
                    ui.horizontal_wrapped(|ui| {
                        ui.checkbox(&mut state.looping, "Loop");
                        ui.label(egui::RichText::new("Speed").small().color(TEXT_DIM));
                        ui.add(
                            egui::DragValue::new(&mut state.speed)
                                .range(0.01..=8.0)
                                .speed(0.01)
                                .suffix("x"),
                        );
                    });
                });

                let clip_duration = manifest
                    .and_then(|manifest| {
                        resolved_clip_for_ref(manifest, &referenced_clip_manifests, &state.clip)
                    })
                    .map(|clip| clip.duration_seconds.max(0.0));
                property_row(ui, "Start", |ui| {
                    let max = clip_duration.unwrap_or(86_400.0).max(0.01);
                    ui.add(
                        egui::DragValue::new(&mut state.range.start_seconds)
                            .range(0.0..=max)
                            .speed(0.01)
                            .suffix(" s"),
                    );
                });
                property_row(ui, "End", |ui| {
                    ui.horizontal_wrapped(|ui| {
                        let mut limited = state.range.end_seconds.is_some();
                        if ui.checkbox(&mut limited, "Custom").changed() {
                            state.range.end_seconds = limited.then(|| {
                                clip_duration
                                    .unwrap_or(state.range.start_seconds + 1.0)
                                    .max(state.range.start_seconds + 0.01)
                            });
                        }
                        if let Some(end) = state.range.end_seconds.as_mut() {
                            let max = clip_duration
                                .unwrap_or(86_400.0)
                                .max(state.range.start_seconds + 0.01);
                            ui.add(
                                egui::DragValue::new(end)
                                    .range((state.range.start_seconds + 0.01)..=max)
                                    .speed(0.01)
                                    .suffix(" s"),
                            );
                        } else if let Some(duration) = clip_duration {
                            ui.label(
                                egui::RichText::new(format!("Clip end ({duration:.2} s)"))
                                    .small()
                                    .color(TEXT_DIM),
                            );
                        }
                    });
                });
                ui.add_space(4.0);
                ui.separator();
            });
        }
        controller.default_state = default_state;

        if let Some(index) = remove_state {
            let removed = controller.states.remove(index);
            if controller.default_state == removed.id {
                controller.default_state = controller
                    .states
                    .first()
                    .map(|state| state.id.clone())
                    .unwrap_or_default();
            }
            if selected_state == removed.id {
                selected_state = controller
                    .states
                    .first()
                    .map(|state| state.id.clone())
                    .unwrap_or_default();
                preview_action = Some(PreviewAction::Stop);
            }
        }

        if ui
            .add_enabled(
                manifest.is_some_and(|manifest| !manifest.clips.is_empty()),
                egui::Button::new(format!("{} Add State", icons::PLUS.as_str())),
            )
            .on_hover_text("Add an animation state")
            .clicked()
        {
            if let Some(manifest) = manifest {
                if let Some(clip) = preferred_clip(manifest, controller.skeleton.as_ref()) {
                    let id = unique_state_id(
                        &clip.label,
                        controller.states.iter().map(|state| state.id.as_str()),
                    );
                    controller.states.push(AnimationStateDesc {
                        id: id.clone(),
                        label: clip.label.clone(),
                        clip: clip_ref(manifest, clip),
                        ..Default::default()
                    });
                    if controller.default_state.trim().is_empty() {
                        controller.default_state.clone_from(&id);
                    }
                    selected_state = id;
                    scrub_seconds = 0.0;
                }
            }
        }

        let default_choices = controller
            .states
            .iter()
            .map(|state| (state.id.clone(), state_display_name(state).to_string()))
            .collect::<Vec<_>>();
        property_row(ui, "Default state", |ui| {
            let selected = controller
                .state(&controller.default_state)
                .map(state_display_name)
                .unwrap_or("None");
            egui::ComboBox::from_id_salt(("animation_default", entity))
                .selected_text(selected)
                .width(text_field_width(ui))
                .show_ui(ui, |ui| {
                    for (id, label) in &default_choices {
                        ui.selectable_value(&mut controller.default_state, id.clone(), label);
                    }
                });
        });

        ui.add_space(8.0);
        ui.strong("Preview");
        ui.separator();
        if controller.state(&selected_state).is_none() {
            selected_state = controller
                .state(&controller.default_state)
                .map(|state| state.id.clone())
                .or_else(|| controller.states.first().map(|state| state.id.clone()))
                .unwrap_or_default();
        }
        let preview_choices = controller
            .states
            .iter()
            .map(|state| (state.id.clone(), state_display_name(state).to_string()))
            .collect::<Vec<_>>();
        let runtime_supported = manifest.is_some_and(|manifest| manifest.runtime_supported);
        let preview_valid = controller.state(&selected_state).is_some_and(|state| {
            state.clip.is_resolved()
                && manifest.is_some_and(|manifest| {
                    resolved_clip_for_ref(manifest, &referenced_clip_manifests, &state.clip)
                        .is_some_and(|clip| {
                            clip_compatibility(manifest, controller.skeleton.as_ref(), clip)
                                != ClipCompatibility::Mismatch
                        })
                })
        });
        property_row(ui, "State", |ui| {
            let selected = controller
                .state(&selected_state)
                .map(state_display_name)
                .unwrap_or("None");
            egui::ComboBox::from_id_salt(("animation_preview_state", entity))
                .selected_text(selected)
                .width(text_field_width(ui))
                .show_ui(ui, |ui| {
                    for (id, label) in &preview_choices {
                        if ui
                            .selectable_value(&mut selected_state, id.clone(), label)
                            .clicked()
                        {
                            scrub_seconds = controller
                                .state(id)
                                .map(|state| state.range.start_seconds)
                                .unwrap_or(0.0);
                            preview_action = Some(PreviewAction::Set(
                                game::animation::AnimationPreviewRequest {
                                    actor: entity,
                                    state_id: id.clone(),
                                    seek_seconds: Some(scrub_seconds),
                                    playing: false,
                                },
                            ));
                        }
                    }
                });
        });

        let active_for_actor = active_preview
            .as_ref()
            .is_some_and(|request| request.actor == entity);
        let active_playing = active_preview.as_ref().is_some_and(|request| {
            request.actor == entity && request.state_id == selected_state && request.playing
        });
        property_row(ui, "Transport", |ui| {
            ui.horizontal_wrapped(|ui| {
                let icon = if active_playing {
                    icons::PAUSE
                } else {
                    icons::PLAY
                };
                let label = if active_playing { "Pause" } else { "Play" };
                if ui
                    .add_enabled(
                        runtime_supported && preview_valid,
                        egui::Button::new(format!("{} {label}", icon.as_str())),
                    )
                    .clicked()
                {
                    preview_action = Some(PreviewAction::Set(
                        game::animation::AnimationPreviewRequest {
                            actor: entity,
                            state_id: selected_state.clone(),
                            seek_seconds: Some(scrub_seconds),
                            playing: !active_playing,
                        },
                    ));
                }
                if ui
                    .add_enabled(
                        active_for_actor,
                        egui::Button::new(format!("{} Stop", icons::STOP.as_str())),
                    )
                    .clicked()
                {
                    preview_action = Some(PreviewAction::Stop);
                }
            });
        });

        if let Some(state) = controller.state(&selected_state) {
            let clip_end = manifest
                .and_then(|manifest| {
                    resolved_clip_for_ref(manifest, &referenced_clip_manifests, &state.clip)
                })
                .map(|clip| clip.duration_seconds)
                .unwrap_or(state.range.start_seconds + 1.0);
            let end = state
                .range
                .end_seconds
                .unwrap_or(clip_end)
                .max(state.range.start_seconds + 0.01);
            scrub_seconds = scrub_seconds.clamp(state.range.start_seconds, end);
            property_row(ui, "Time", |ui| {
                ui.horizontal(|ui| {
                    let response = ui.add_enabled(
                        runtime_supported && preview_valid,
                        egui::Slider::new(&mut scrub_seconds, state.range.start_seconds..=end)
                            .show_value(false),
                    );
                    ui.label(
                        egui::RichText::new(format!("{scrub_seconds:.2} / {end:.2} s"))
                            .small()
                            .color(TEXT_DIM),
                    );
                    if response.changed() {
                        preview_action = Some(PreviewAction::Set(
                            game::animation::AnimationPreviewRequest {
                                actor: entity,
                                state_id: selected_state.clone(),
                                seek_seconds: Some(scrub_seconds),
                                playing: false,
                            },
                        ));
                    }
                });
            });
        }
        if manifest.is_some_and(|manifest| !manifest.runtime_supported) {
            ui.label(
                egui::RichText::new("Preview unavailable for this source format")
                    .small()
                    .color(WARNING),
            );
        }
    });

    controller.default_crossfade_seconds = controller.default_crossfade_seconds.max(0.0);
    for state in &mut controller.states {
        state.speed = state.speed.max(0.01);
        state.range.start_seconds = state.range.start_seconds.max(0.0);
        if let Some(end) = state.range.end_seconds.as_mut() {
            *end = end.max(state.range.start_seconds + 0.01);
        }
    }

    let changed = controller != original;
    if changed {
        set_animation_controller_with_history(world, entity, controller.clone());
    }
    let active_state_still_exists = active_preview
        .as_ref()
        .filter(|request| request.actor == entity)
        .is_none_or(|request| controller.state(&request.state_id).is_some());
    if !active_state_still_exists {
        game::animation::stop_animation_preview(world);
    }
    match preview_action {
        Some(PreviewAction::Set(request)) if controller.state(&request.state_id).is_some() => {
            if world
                .get_resource::<game::animation::AnimationPreviewRuntime>()
                .is_none()
            {
                set_status(world, "Animation preview failed: runtime is unavailable");
            } else if let Err(error) = game::animation::set_animation_preview(world, request) {
                set_status(world, format!("Animation preview failed: {error}"));
            }
        }
        Some(PreviewAction::Set(_)) | Some(PreviewAction::Stop) => {
            stop_preview_if_actor(world, entity);
        }
        None => {}
    }
    if let Some(mut state) = world.get_resource_mut::<AnimationInspectorState>() {
        if selected_state.is_empty() {
            state.selected_state.remove(&entity);
        } else {
            state.selected_state.insert(entity, selected_state);
        }
        state.scrub_seconds.insert(entity, scrub_seconds);
    }
    apply_component_card_response(world, entity, card_response);
}

fn manifest_summary(ui: &mut egui::Ui, manifest: &Result<AnimationManifest, String>) {
    match manifest {
        Ok(manifest) => {
            let color = if manifest.runtime_supported {
                SUCCESS
            } else {
                WARNING
            };
            ui.horizontal_wrapped(|ui| {
                ui.label(
                    egui::RichText::new(format!(
                        "{} | {} rig{} | {} clip{}",
                        manifest.source.format.to_ascii_uppercase(),
                        manifest.skeletons.len(),
                        if manifest.skeletons.len() == 1 {
                            ""
                        } else {
                            "s"
                        },
                        manifest.clips.len(),
                        if manifest.clips.len() == 1 { "" } else { "s" },
                    ))
                    .small()
                    .color(color),
                );
            });
            if let Some(diagnostic) = manifest.diagnostics.first() {
                ui.label(
                    egui::RichText::new(&diagnostic.message)
                        .small()
                        .color(WARNING),
                )
                .on_hover_text(&diagnostic.repair);
            }
        }
        Err(error) => {
            ui.label(
                egui::RichText::new("Animation manifest unavailable")
                    .small()
                    .color(ERROR),
            )
            .on_hover_text(error);
        }
    }
}

fn manifest_for_actor(world: &World, entity: Entity) -> Result<AnimationManifest, String> {
    let model = world
        .get::<SkinnedMeshRenderer>(entity)
        .ok_or_else(|| "actor has no SkinnedMeshRenderer".to_string())?;
    let registry = world
        .get_resource::<AssetRegistry>()
        .ok_or_else(|| "asset registry is unavailable".to_string())?;
    let normalized = model.path.trim_start_matches("./");
    let with_assets = (!normalized.starts_with("assets/")).then(|| format!("assets/{normalized}"));
    let without_assets = normalized.strip_prefix("assets/");
    let record = (!model.asset_id.trim().is_empty())
        .then(|| find_asset_by_id(registry, &model.asset_id))
        .flatten()
        .or_else(|| find_asset_by_path(registry, normalized))
        .or_else(|| {
            with_assets
                .as_deref()
                .and_then(|path| find_asset_by_path(registry, path))
        })
        .or_else(|| without_assets.and_then(|path| find_asset_by_path(registry, path)))
        .ok_or_else(|| format!("model `{}` is not indexed", model.path))?;
    let path = record
        .import_settings
        .animation_manifest_path
        .as_deref()
        .ok_or_else(|| format!("model `{}` has no animation manifest", model.path))?;
    load_animation_manifest(path)
}

fn referenced_clip_manifests(
    world: &World,
    controller: &AnimationControllerDesc,
    primary: Option<&AnimationManifest>,
) -> HashMap<String, AnimationManifest> {
    let Some(registry) = world.get_resource::<AssetRegistry>() else {
        return HashMap::new();
    };
    let primary_id = primary.map(|manifest| manifest.asset_id.as_str());
    let mut manifests = HashMap::new();
    for asset_id in controller
        .states
        .iter()
        .map(|state| state.clip.asset_id.as_str())
        .filter(|asset_id| !asset_id.trim().is_empty() && Some(*asset_id) != primary_id)
    {
        if manifests.contains_key(asset_id) {
            continue;
        }
        let Some(record) = find_asset_by_id(registry, asset_id) else {
            continue;
        };
        let Some(path) = record.import_settings.animation_manifest_path.as_deref() else {
            continue;
        };
        if let Ok(manifest) = load_animation_manifest(path) {
            manifests.insert(asset_id.to_string(), manifest);
        }
    }
    manifests
}

fn controller_from_manifest(manifest: &AnimationManifest) -> AnimationControllerDesc {
    let skeleton = manifest
        .skeletons
        .first()
        .map(|record| skeleton_ref(manifest, record));
    let clip = preferred_clip(manifest, skeleton.as_ref());
    let states = clip
        .map(|clip| {
            let id = unique_state_id(&clip.label, std::iter::empty::<&str>());
            vec![AnimationStateDesc {
                id,
                label: clip.label.clone(),
                clip: clip_ref(manifest, clip),
                ..Default::default()
            }]
        })
        .unwrap_or_default();
    let default_state = states
        .first()
        .map(|state| state.id.clone())
        .unwrap_or_default();
    AnimationControllerDesc {
        skeleton,
        states,
        default_state,
        ..Default::default()
    }
}

fn skeleton_ref(
    manifest: &AnimationManifest,
    skeleton: &AnimationSkeletonRecord,
) -> EditorAssetRef {
    EditorAssetRef::new(
        manifest.asset_id.clone(),
        skeleton.id.clone(),
        skeleton.label.clone(),
    )
    .with_source_path(manifest.source.path.clone())
}

fn clip_ref(manifest: &AnimationManifest, clip: &AnimationClipRecord) -> EditorAssetRef {
    EditorAssetRef::new(
        manifest.asset_id.clone(),
        clip.id.clone(),
        clip.label.clone(),
    )
    .with_source_path(manifest.source.path.clone())
}

fn clip_for_ref<'a>(
    manifest: &'a AnimationManifest,
    reference: &EditorAssetRef,
) -> Option<&'a AnimationClipRecord> {
    if manifest.asset_id != reference.asset_id {
        return None;
    }
    manifest
        .clips
        .iter()
        .find(|clip| clip.id == reference.sub_asset_id)
}

fn resolved_clip_for_ref<'a>(
    primary: &'a AnimationManifest,
    referenced: &'a HashMap<String, AnimationManifest>,
    reference: &EditorAssetRef,
) -> Option<&'a AnimationClipRecord> {
    if primary.asset_id == reference.asset_id {
        clip_for_ref(primary, reference)
    } else {
        referenced
            .get(&reference.asset_id)
            .and_then(|manifest| clip_for_ref(manifest, reference))
    }
}

fn preferred_clip<'a>(
    manifest: &'a AnimationManifest,
    skeleton: Option<&EditorAssetRef>,
) -> Option<&'a AnimationClipRecord> {
    manifest
        .clips
        .iter()
        .find(|clip| clip_compatibility(manifest, skeleton, clip) == ClipCompatibility::Compatible)
        .or_else(|| {
            manifest.clips.iter().find(|clip| {
                clip_compatibility(manifest, skeleton, clip) == ClipCompatibility::Unknown
            })
        })
}

fn clip_compatibility(
    manifest: &AnimationManifest,
    skeleton: Option<&EditorAssetRef>,
    clip: &AnimationClipRecord,
) -> ClipCompatibility {
    let Some(skeleton) = skeleton else {
        return ClipCompatibility::Unknown;
    };
    let Some(skeleton) = manifest
        .skeletons
        .iter()
        .find(|record| record.id == skeleton.sub_asset_id)
    else {
        return ClipCompatibility::Unknown;
    };
    let Some(target) = clip.target_skeleton_signature.as_ref() else {
        return ClipCompatibility::Unknown;
    };
    if target == &skeleton.signature {
        ClipCompatibility::Compatible
    } else {
        ClipCompatibility::Mismatch
    }
}

fn unique_state_id<'a>(label: &str, existing: impl Iterator<Item = &'a str>) -> String {
    let existing = existing.collect::<Vec<_>>();
    let mut base = String::new();
    for character in label.chars() {
        if character.is_ascii_alphanumeric() {
            base.push(character.to_ascii_lowercase());
        } else if !base.ends_with('_') {
            base.push('_');
        }
    }
    let base = base.trim_matches('_');
    let base = if base.is_empty() { "state" } else { base };
    if !existing.contains(&base) {
        return base.to_string();
    }
    (2..)
        .map(|suffix| format!("{base}_{suffix}"))
        .find(|candidate| !existing.iter().any(|id| *id == candidate))
        .expect("unbounded state ID suffix search must find an ID")
}

fn state_display_name(state: &AnimationStateDesc) -> &str {
    if state.label.trim().is_empty() {
        state.id.as_str()
    } else {
        state.label.as_str()
    }
}

fn set_status(world: &mut World, status: impl Into<String>) {
    if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
        scene_io.set_status(status);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use shared::{AnimationManifestSource, AnimationSkeletonSignature, AnimationSourceFingerprint};

    fn manifest() -> AnimationManifest {
        let signature = AnimationSkeletonSignature::new("rig-a");
        AnimationManifest {
            schema_version: 1,
            asset_id: "model-id".into(),
            label: "Robot".into(),
            default_animation_clip_id: None,
            source: AnimationManifestSource {
                path: "assets/models/robot.glb".into(),
                format: "glb".into(),
                fingerprint: AnimationSourceFingerprint {
                    byte_len: 42,
                    modified_unix_secs: 1,
                    content_hash: "hash".into(),
                },
                dependencies: Vec::new(),
            },
            runtime_supported: true,
            skeletons: vec![AnimationSkeletonRecord {
                id: "animation:skeleton:0:rig".into(),
                label: "Rig".into(),
                source_index: 0,
                signature: signature.clone(),
                joint_paths: vec!["Root".into()],
            }],
            clips: vec![
                AnimationClipRecord {
                    id: "animation:clip:0:idle".into(),
                    label: "Idle Loop".into(),
                    source_index: 0,
                    duration_seconds: 2.0,
                    target_skeleton_signature: Some(signature),
                    events: Vec::new(),
                },
                AnimationClipRecord {
                    id: "animation:clip:1:other".into(),
                    label: "Other".into(),
                    source_index: 1,
                    duration_seconds: 1.0,
                    target_skeleton_signature: Some(AnimationSkeletonSignature::new("rig-b")),
                    events: Vec::new(),
                },
            ],
            diagnostics: Vec::new(),
        }
    }

    #[test]
    fn manifest_defaults_select_a_compatible_rig_and_clip() {
        let manifest = manifest();
        let controller = controller_from_manifest(&manifest);
        assert_eq!(controller.skeleton.as_ref().unwrap().label, "Rig");
        assert_eq!(controller.states.len(), 1);
        assert_eq!(controller.states[0].id, "idle_loop");
        assert_eq!(controller.default_state, "idle_loop");
        assert_eq!(
            controller.states[0].clip.source_path.as_deref(),
            Some("assets/models/robot.glb")
        );
    }

    #[test]
    fn state_ids_are_stable_and_unique() {
        assert_eq!(
            unique_state_id("Idle Loop", std::iter::empty()),
            "idle_loop"
        );
        assert_eq!(
            unique_state_id("Idle Loop", ["idle_loop"].into_iter()),
            "idle_loop_2"
        );
        assert_eq!(
            unique_state_id("---", ["state", "state_2"].into_iter()),
            "state_3"
        );
    }

    #[test]
    fn compatibility_distinguishes_matching_and_foreign_rigs() {
        let manifest = manifest();
        let skeleton = skeleton_ref(&manifest, &manifest.skeletons[0]);
        assert_eq!(
            clip_compatibility(&manifest, Some(&skeleton), &manifest.clips[0]),
            ClipCompatibility::Compatible
        );
        assert_eq!(
            clip_compatibility(&manifest, Some(&skeleton), &manifest.clips[1]),
            ClipCompatibility::Mismatch
        );
        assert_eq!(
            clip_compatibility(&manifest, None, &manifest.clips[0]),
            ClipCompatibility::Unknown
        );
    }

    #[test]
    fn clip_lookup_uses_stable_parent_asset_identity() {
        let primary = manifest();
        let mut foreign = manifest();
        foreign.asset_id = "foreign-model-id".into();
        foreign.clips[0].label = "Foreign Idle".into();
        let reference = EditorAssetRef::new(
            foreign.asset_id.clone(),
            foreign.clips[0].id.clone(),
            foreign.clips[0].label.clone(),
        );
        let referenced = HashMap::from([(foreign.asset_id.clone(), foreign)]);

        assert!(clip_for_ref(&primary, &reference).is_none());
        assert_eq!(
            resolved_clip_for_ref(&primary, &referenced, &reference)
                .map(|clip| clip.label.as_str()),
            Some("Foreign Idle")
        );
    }

    #[test]
    fn preview_does_not_dirty_authored_data_or_history() {
        let mut world = World::new();
        world.init_resource::<game::animation::AnimationPreviewRuntime>();
        world.init_resource::<crate::history::EditorHistory>();
        world.init_resource::<crate::scene_io::SceneIo>();
        let controller = controller_from_manifest(&manifest());
        let actor = world.spawn(controller.clone()).id();

        game::animation::set_animation_preview(
            &mut world,
            game::animation::AnimationPreviewRequest {
                actor,
                state_id: controller.default_state.clone(),
                seek_seconds: Some(0.5),
                playing: true,
            },
        )
        .unwrap();
        game::animation::stop_animation_preview(&mut world);

        assert_eq!(
            world.get::<AnimationControllerDesc>(actor),
            Some(&controller)
        );
        assert_eq!(
            world
                .resource::<crate::history::EditorHistory>()
                .undo_depth(),
            0
        );
        assert!(!world.resource::<crate::scene_io::SceneIo>().dirty);
    }
}
