//! Asset browser dock panel: project tree, file grid/list, and asset details.

use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use bevy::prelude::*;
use bevy_egui::egui;
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
use egui_phosphor_icons::{icons, Icon};
use shared::{
    ColorDesc, EditorAssetRef, MaterialAlphaMode, MaterialAsset, MaterialDesc,
    MaterialInstanceAsset, MaterialParameter, MaterialParameterValue, MaterialRef,
    MaterialShaderKind, MaterialTextureBinding, ShaderPropertyDesc, ShaderPropertyType,
    ShaderSchemaAsset,
};

use crate::assets::{
    AssetSelection, AssetSubAssetKind, EditorAsset, EditorAssetKind, EditorAssets, ASSETS_ROOT,
    BUILTINS_FOLDER,
};
use crate::project::collaboration::{
    file_status_indicator_ui, publish_authored_file, CollaborationState, FileSnapshot,
    FileWriteIntent,
};
use crate::scene_io::{SceneIo, SceneIoRequest};

use super::state::{
    AssetBrowserUiState, AssetBrowserView, AssetDeleteRequest, AssetKindFilter, AssetSort,
    ImportSettingsDraft, MaterialAssetDraft, MaterialInstanceAssetDraft,
};
use crate::asset_db::{
    ensure_asset_record, find_asset_by_path, find_asset_mut_by_path, save_registry,
    update_import_settings, ImportSettings, MaterialImportPolicy, ModelHierarchyMode,
    ModelPlacementMode,
};
use crate::assets::animation::load_animation_manifest;
use crate::assets::operators::{
    apply_material_operator, apply_texture_operator, assign_animation_clip_operator,
    place_asset_operator, place_subasset_operator,
};
use crate::assets::static_mesh::{
    load_static_mesh_manifest, material_id_from_label, part_id_from_label,
};
use crate::assets::{
    asset_cache_key, draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon,
    prefetch_folder_thumbnails, refresh_model_artifacts, AssetThumbnailCache,
    ThumbnailCacheSnapshot, ThumbnailStudio,
};
use crate::ui::helpers::asset_label;
use crate::ui::theme::{panel_heading, ACCENT, BORDER, ELEVATED_BG, TEXT, TEXT_DIM, WIDGET_BG};
use crate::ui::widgets::{icon_button_small, tool_button};

const FOOTER_HEIGHT: f32 = 64.0;
const TOOLBAR_HEIGHT: f32 = 40.0;
const DETAILS_WIDTH: f32 = 260.0;
const DETAILS_MIN_PANEL_WIDTH: f32 = 760.0;
const DETAILS_MIN_WIDTH: f32 = 200.0;
const TREE_MIN_PANEL_WIDTH: f32 = 540.0;
const TREE_MIN_WIDTH: f32 = 150.0;
const TREE_MAX_WIDTH: f32 = 240.0;
const MIN_CONTENT_WIDTH: f32 = 160.0;
const COMPACT_LIST_WIDTH: f32 = 560.0;
const PHOSPHOR: &str = "phosphor-regular";

#[derive(Clone)]
struct FolderSnapshot {
    path: String,
    name: String,
    parent: Option<String>,
}

#[derive(Clone)]
struct AssetRow {
    selection: AssetSelection,
    asset: EditorAsset,
    file_size: Option<u64>,
    modified: Option<SystemTime>,
}

#[derive(Clone)]
struct EmbeddedAsset {
    selection: AssetSelection,
    label: String,
    kind: AssetSubAssetKind,
    detail: String,
    texture_path: Option<String>,
    thumbnail_key: String,
    mesh_label: Option<String>,
    material_label: Option<String>,
    requires_skinned_hierarchy: bool,
}

#[derive(Clone, Debug)]
struct MaterialTextureCandidate {
    label: String,
    detail: String,
    path: String,
    reference: EditorAssetRef,
    selection: AssetSelection,
}

#[derive(Clone, Copy)]
enum TextureSlotSelection<'a> {
    Path(Option<&'a str>),
    Reference(Option<&'a EditorAssetRef>),
}

#[derive(Default)]
struct TextureSlotResponse {
    selected: Option<MaterialTextureCandidate>,
    clear: bool,
    accepted_drop: bool,
}

fn fit_width(ui: &egui::Ui, min: f32, max: f32) -> f32 {
    let available = ui.available_width().max(1.0);
    available.min(max).max(min.min(available))
}

fn icon_text(icon: Icon, size: f32) -> egui::RichText {
    egui::RichText::new(icon.as_str()).font(egui::FontId::new(
        size,
        egui::FontFamily::Name(PHOSPHOR.into()),
    ))
}

fn exact_region<R>(
    ui: &mut egui::Ui,
    size: egui::Vec2,
    layout: egui::Layout,
    add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
    let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect).layout(layout));
    add_contents(&mut child)
}

pub fn asset_browser_ui(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
) {
    let available_width = ui.available_width().max(1.0);
    let available_height = ui.available_height().max(1.0);
    let body_height = (available_height - TOOLBAR_HEIGHT - FOOTER_HEIGHT - 4.0).max(40.0);

    exact_region(
        ui,
        egui::vec2(available_width, available_height),
        egui::Layout::top_down(egui::Align::Min),
        |ui| {
            exact_region(
                ui,
                egui::vec2(available_width, TOOLBAR_HEIGHT),
                egui::Layout::left_to_right(egui::Align::Center),
                |ui| {
                    asset_toolbar(world, ui);
                },
            );
            ui.separator();

            let (current_folder, selected, folders, assets, cache_snapshot, state_snapshot) = {
                let assets = world.resource::<EditorAssets>();
                let cache = world.resource::<AssetThumbnailCache>();
                let state = world.resource::<AssetBrowserUiState>();
                let folders: Vec<FolderSnapshot> = assets
                    .folders
                    .iter()
                    .map(|folder| FolderSnapshot {
                        path: folder.path.clone(),
                        name: folder.name.clone(),
                        parent: folder.parent.clone(),
                    })
                    .collect();
                let asset_rows: Vec<AssetRow> = assets
                    .assets
                    .iter()
                    .map(|asset| AssetRow {
                        selection: AssetSelection::from_asset(asset),
                        asset: asset.clone(),
                        file_size: asset.path.as_deref().and_then(file_size),
                        modified: asset.path.as_deref().and_then(modified_time),
                    })
                    .collect();
                (
                    assets.current_folder.clone(),
                    assets.selected.clone(),
                    folders,
                    asset_rows,
                    cache.snapshot(),
                    state_snapshot(state),
                )
            };

            prefetch_folder_thumbnails(world, &current_folder);

            let show_tree = available_width >= TREE_MIN_PANEL_WIDTH;
            let show_details =
                state_snapshot.show_details && available_width >= DETAILS_MIN_PANEL_WIDTH;
            let tree_width = if show_tree {
                (available_width * 0.22).clamp(TREE_MIN_WIDTH, TREE_MAX_WIDTH)
            } else {
                0.0
            };
            let details_width = if show_details {
                DETAILS_WIDTH.min((available_width * 0.28).max(DETAILS_MIN_WIDTH))
            } else {
                0.0
            };
            let separator_budget = (show_tree as u8 as f32 + show_details as u8 as f32) * 10.0;
            let content_width = (available_width - tree_width - details_width - separator_budget)
                .max(MIN_CONTENT_WIDTH.min(available_width));

            exact_region(
                ui,
                egui::vec2(available_width, body_height),
                egui::Layout::left_to_right(egui::Align::Min),
                |ui| {
                    if show_tree {
                        exact_region(
                            ui,
                            egui::vec2(tree_width, body_height),
                            egui::Layout::top_down(egui::Align::Min),
                            |ui| {
                                ui.label(panel_heading("Project"));
                                egui::ScrollArea::vertical()
                                    .id_salt("asset_folder_tree")
                                    .max_height(ui.available_height())
                                    .auto_shrink([false, false])
                                    .show(ui, |ui| {
                                        for root in [BUILTINS_FOLDER, ASSETS_ROOT] {
                                            folder_tree_branch(
                                                world,
                                                ui,
                                                &folders,
                                                root,
                                                0,
                                                &current_folder,
                                            );
                                        }
                                    });
                            },
                        );
                        ui.separator();
                    }

                    exact_region(
                        ui,
                        egui::vec2(content_width, body_height),
                        egui::Layout::top_down(egui::Align::Min),
                        |ui| {
                            content_header(world, ui, &current_folder, &folders, &assets);
                            egui::ScrollArea::vertical()
                                .id_salt("asset_content_area")
                                .max_height(ui.available_height())
                                .auto_shrink([false, false])
                                .show(ui, |ui| {
                                    let child_folders = child_folders_for_content(
                                        &folders,
                                        &current_folder,
                                        &state_snapshot.search,
                                    );
                                    let visible_assets =
                                        visible_assets(&assets, &current_folder, &state_snapshot);
                                    prefetch_asset_row_thumbnails(world, &visible_assets);

                                    if child_folders.is_empty() && visible_assets.is_empty() {
                                        empty_content(ui, &state_snapshot.search);
                                    } else if state_snapshot.view == AssetBrowserView::Grid {
                                        let thumbnail_size = state_snapshot
                                            .thumbnail_size
                                            .min((content_width - 26.0).max(48.0));
                                        asset_grid(
                                            world,
                                            ui,
                                            selected_entities,
                                            &selected,
                                            &child_folders,
                                            &visible_assets,
                                            &cache_snapshot,
                                            thumbnail_size,
                                        );
                                    } else {
                                        asset_list(
                                            world,
                                            ui,
                                            selected_entities,
                                            &selected,
                                            &child_folders,
                                            &visible_assets,
                                            &cache_snapshot,
                                        );
                                    }
                                });
                        },
                    );

                    if show_details {
                        ui.separator();
                        exact_region(
                            ui,
                            egui::vec2(details_width, body_height),
                            egui::Layout::top_down(egui::Align::Min),
                            |ui| {
                                egui::ScrollArea::vertical()
                                    .id_salt("asset_details_area")
                                    .max_height(ui.available_height())
                                    .auto_shrink([false, false])
                                    .show(ui, |ui| {
                                        details_panel(world, ui, selected_entities);
                                    });
                            },
                        );
                    }
                },
            );

            ui.separator();
            exact_region(
                ui,
                egui::vec2(available_width, FOOTER_HEIGHT),
                egui::Layout::top_down(egui::Align::Min),
                |ui| {
                    egui::Frame::new()
                        .fill(ELEVATED_BG.linear_multiply(0.5))
                        .inner_margin(egui::Margin::symmetric(8, 6))
                        .show(ui, |ui| {
                            ui.set_max_height(FOOTER_HEIGHT - 12.0);
                            selection_footer(world, ui, selected_entities);
                        });
                },
            );
        },
    );

    draw_delete_modal(world, ui.ctx());
}

#[derive(Clone)]
struct AssetBrowserStateSnapshot {
    search: String,
    view: AssetBrowserView,
    sort: AssetSort,
    kind_filter: AssetKindFilter,
    thumbnail_size: f32,
    recursive: bool,
    show_details: bool,
}

fn state_snapshot(state: &AssetBrowserUiState) -> AssetBrowserStateSnapshot {
    AssetBrowserStateSnapshot {
        search: state.search.clone(),
        view: state.view,
        sort: state.sort,
        kind_filter: state.kind_filter,
        thumbnail_size: state.thumbnail_size,
        recursive: state.recursive,
        show_details: state.show_details,
    }
}

fn embedded_assets_for_asset(world: &World, asset: &EditorAsset) -> Vec<EmbeddedAsset> {
    if !matches!(asset.kind, EditorAssetKind::Model) {
        return Vec::new();
    }
    let Some(parent_path) = asset.path.as_ref() else {
        return Vec::new();
    };
    let Some(record) = world
        .get_resource::<crate::asset_db::AssetRegistry>()
        .and_then(|registry| find_asset_by_path(registry, parent_path))
    else {
        return Vec::new();
    };
    let mut embedded = Vec::new();
    let static_manifest = record
        .import_settings
        .static_mesh_manifest_path
        .as_deref()
        .and_then(|path| load_static_mesh_manifest(path).ok());
    let legacy_skinned_primitives = static_manifest
        .as_ref()
        .filter(|manifest| manifest.schema_version < 2)
        .map(|_| crate::assets::gltf_skinned_primitive_labels(parent_path.as_str()))
        .unwrap_or_default();
    if let Some(manifest) = static_manifest.as_ref() {
        for part in &manifest.parts {
            let mesh_id = part_effective_id(part);
            let material_label = part.material_label.clone();
            let requires_skinned_hierarchy = manifest.metadata.animation_count > 0
                || part.skinned
                || legacy_skinned_primitives.contains(&part.mesh_label);
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: mesh_id,
                    label: part.name.clone(),
                    kind: AssetSubAssetKind::Mesh,
                    source_path: None,
                },
                label: part.name.clone(),
                kind: AssetSubAssetKind::Mesh,
                detail: if requires_skinned_hierarchy {
                    "Skinned | dedicated renderer".to_string()
                } else {
                    part.source_mesh
                        .clone()
                        .unwrap_or_else(|| part.mesh_label.clone())
                },
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Mesh,
                    &part_effective_id(part),
                ),
                mesh_label: Some(part.mesh_label.clone()),
                material_label,
                requires_skinned_hierarchy,
            });
        }

        let mut material_ids = HashSet::new();
        for part in &manifest.parts {
            let Some(material_id) = part_effective_material_id(part) else {
                continue;
            };
            if !material_ids.insert(material_id.clone()) {
                continue;
            }
            let label = if part.material_slot_name.trim().is_empty() {
                part.material_label
                    .clone()
                    .unwrap_or_else(|| "Source Material".to_string())
            } else {
                part.material_slot_name.clone()
            };
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: material_id,
                    label: label.clone(),
                    kind: AssetSubAssetKind::Material,
                    source_path: None,
                },
                label,
                kind: AssetSubAssetKind::Material,
                detail: "Embedded source material".to_string(),
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Material,
                    &part_effective_material_id(part).unwrap_or_else(|| {
                        material_id_from_label(
                            part.material_label
                                .as_deref()
                                .unwrap_or(&part.material_slot_name),
                        )
                    }),
                ),
                mesh_label: None,
                material_label: part.material_label.clone(),
                requires_skinned_hierarchy: false,
            });
        }

        let mut texture_paths = HashSet::new();
        for dependency in &manifest.source.dependencies {
            if !is_texture_path(dependency) || !texture_paths.insert(dependency.clone()) {
                continue;
            }
            let label = Path::new(dependency)
                .file_stem()
                .and_then(|stem| stem.to_str())
                .unwrap_or(dependency)
                .to_string();
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: format!("texture:{}", stable_subasset_slug(dependency)),
                    label: label.clone(),
                    kind: AssetSubAssetKind::Texture,
                    source_path: Some(dependency.clone()),
                },
                label,
                kind: AssetSubAssetKind::Texture,
                detail: dependency.clone(),
                texture_path: Some(dependency.clone()),
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Texture,
                    &stable_subasset_slug(dependency),
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
    }

    let animation_manifest = record
        .import_settings
        .animation_manifest_path
        .as_deref()
        .and_then(|path| load_animation_manifest(path).ok());
    if let Some(manifest) = animation_manifest {
        for skeleton in manifest.skeletons {
            let signature = skeleton.signature.0;
            let signature_short = signature.get(..8).unwrap_or(&signature);
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: skeleton.id.clone(),
                    label: skeleton.label.clone(),
                    kind: AssetSubAssetKind::Skeleton,
                    source_path: Some(parent_path.clone()),
                },
                label: skeleton.label,
                kind: AssetSubAssetKind::Skeleton,
                detail: format!(
                    "{} joints | rig {}",
                    skeleton.joint_paths.len(),
                    signature_short
                ),
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Skeleton,
                    &skeleton.id,
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
        for clip in manifest.clips {
            let event_suffix = if clip.events.is_empty() {
                String::new()
            } else {
                format!(" | {} events", clip.events.len())
            };
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: clip.id.clone(),
                    label: clip.label.clone(),
                    kind: AssetSubAssetKind::AnimationClip,
                    source_path: Some(parent_path.clone()),
                },
                label: clip.label,
                kind: AssetSubAssetKind::AnimationClip,
                detail: format!("{:.2}s{event_suffix}", clip.duration_seconds),
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::AnimationClip,
                    &clip.id,
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
    }

    embedded
}

fn embedded_asset_for_selection(
    world: &World,
    selection: &AssetSelection,
) -> Option<EmbeddedAsset> {
    let AssetSelection::SubAsset { parent_path, .. } = selection else {
        return None;
    };
    let asset = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .find(|asset| asset.path.as_deref() == Some(parent_path.as_str()))?
        .clone();
    embedded_assets_for_asset(world, &asset)
        .into_iter()
        .find(|embedded| &embedded.selection == selection)
}

fn has_embedded_assets(world: &World, asset: &EditorAsset) -> bool {
    !embedded_assets_for_asset(world, asset).is_empty()
}

fn part_effective_id(part: &crate::assets::static_mesh::StaticMeshPart) -> String {
    if part.id.trim().is_empty() {
        part_id_from_label(&part.mesh_label)
    } else {
        part.id.clone()
    }
}

fn part_effective_material_id(part: &crate::assets::static_mesh::StaticMeshPart) -> Option<String> {
    part.material_id.clone().or_else(|| {
        part.material_label
            .as_ref()
            .map(|label| material_id_from_label(label))
    })
}

fn is_texture_path(path: &str) -> bool {
    Path::new(path)
        .extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| {
            matches!(
                extension.to_ascii_lowercase().as_str(),
                "png" | "jpg" | "jpeg" | "webp" | "ktx2"
            )
        })
}

fn stable_subasset_slug(label: &str) -> String {
    let mut slug = String::new();
    for ch in label.chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
        } else if !slug.ends_with('_') {
            slug.push('_');
        }
    }
    slug.trim_matches('_').to_string()
}

fn subasset_thumbnail_key(
    parent_path: &str,
    kind: AssetSubAssetKind,
    sub_asset_id: &str,
) -> String {
    format!(
        "{}#{}:{}",
        parent_path,
        subasset_kind_label(kind).to_ascii_lowercase(),
        sub_asset_id
    )
}

fn asset_toolbar(world: &mut World, ui: &mut egui::Ui) {
    ui.horizontal(|ui| {
        if icon_button_small(ui, icons::ARROWS_CLOCKWISE, "Refresh").clicked() {
            world.resource_mut::<EditorAssets>().refresh();
            invalidate_on_catalog_refresh(world);
        }
        if icon_button_small(ui, icons::UPLOAD, "Import assets").clicked() {
            world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::ImportAssets);
        }
        if icon_button_small(ui, icons::ARROW_UP, "Parent folder").clicked() {
            navigate_to_parent(world);
        }

        ui.separator();

        let mut state = world.resource_mut::<AssetBrowserUiState>();
        ui.add(
            egui::TextEdit::singleline(&mut state.search)
                .hint_text("Search assets...")
                .desired_width(fit_width(ui, 120.0, 190.0)),
        );
        ui.checkbox(&mut state.recursive, "Subfolders");

        egui::ComboBox::from_id_salt("asset_kind_filter")
            .selected_text(kind_filter_label(state.kind_filter))
            .show_ui(ui, |ui| {
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::All, "All");
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Model, "Models");
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Texture, "Textures");
                ui.selectable_value(
                    &mut state.kind_filter,
                    AssetKindFilter::Material,
                    "Materials",
                );
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Audio, "Audio");
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Level, "Levels");
                ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Prefab, "Prefabs");
                ui.selectable_value(
                    &mut state.kind_filter,
                    AssetKindFilter::Builtin,
                    "Built-ins",
                );
            });

        egui::ComboBox::from_id_salt("asset_sort")
            .selected_text(sort_label(state.sort))
            .show_ui(ui, |ui| {
                ui.selectable_value(&mut state.sort, AssetSort::Name, "Name");
                ui.selectable_value(&mut state.sort, AssetSort::Kind, "Type");
                ui.selectable_value(&mut state.sort, AssetSort::Modified, "Modified");
                ui.selectable_value(&mut state.sort, AssetSort::Size, "Size");
            });

        ui.separator();
        if tool_button(
            ui,
            icons::GRID_FOUR,
            state.view == AssetBrowserView::Grid,
            "Grid view",
        )
        .clicked()
        {
            state.view = AssetBrowserView::Grid;
        }
        if tool_button(
            ui,
            icons::LIST,
            state.view == AssetBrowserView::List,
            "List view",
        )
        .clicked()
        {
            state.view = AssetBrowserView::List;
        }
        if ui.available_width() > 180.0 {
            ui.label(icon_text(icons::IMAGE_SQUARE, 13.0).color(TEXT_DIM));
            ui.add_sized(
                [96.0, 20.0],
                egui::Slider::new(&mut state.thumbnail_size, 48.0..=112.0).show_value(false),
            );
        }
        ui.checkbox(&mut state.show_details, "Details");
    });
}

fn content_header(
    world: &mut World,
    ui: &mut egui::Ui,
    current_folder: &str,
    folders: &[FolderSnapshot],
    assets: &[AssetRow],
) {
    ui.horizontal_wrapped(|ui| {
        ui.label(panel_heading("Assets"));
        ui.separator();
        breadcrumb(world, ui, current_folder);
    });
    let direct_assets = assets
        .iter()
        .filter(|row| row.asset.folder_path == current_folder)
        .count();
    let direct_folders = folders
        .iter()
        .filter(|folder| folder.parent.as_deref() == Some(current_folder))
        .count();
    ui.small(
        egui::RichText::new(format!("{direct_folders} folders, {direct_assets} assets"))
            .color(TEXT_DIM),
    );
}

fn prefetch_asset_row_thumbnails(world: &mut World, rows: &[AssetRow]) {
    let requests: Vec<(String, String, EditorAssetKind)> = rows
        .iter()
        .filter(|row| {
            matches!(
                row.asset.kind,
                EditorAssetKind::Texture | EditorAssetKind::Model | EditorAssetKind::Material
            )
        })
        .filter_map(|row| {
            Some((
                asset_cache_key(&row.asset),
                row.asset.path.clone()?,
                row.asset.kind.clone(),
            ))
        })
        .collect();
    if requests.is_empty() {
        return;
    }
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
            for (key, path, kind) in &requests {
                match kind {
                    EditorAssetKind::Texture => {
                        cache.request_texture(key.clone(), path.clone(), &asset_server);
                    }
                    EditorAssetKind::Model => {
                        cache.request_model(key.clone(), path.clone(), &asset_server, &mut studio);
                    }
                    EditorAssetKind::Material => {
                        cache.request_material_asset(key.clone(), path.clone(), &mut studio);
                    }
                    _ => {}
                }
            }
        });
    });
}

fn breadcrumb(world: &mut World, ui: &mut egui::Ui, current_folder: &str) {
    let mut parts = Vec::new();
    let mut cursor = Some(current_folder.to_string());
    while let Some(path) = cursor {
        let label = if path == BUILTINS_FOLDER {
            "Built-ins".to_string()
        } else {
            Path::new(&path)
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or(&path)
                .to_string()
        };
        cursor = path.rfind('/').map(|index| path[..index].to_string());
        parts.push((path, label));
    }
    parts.reverse();
    for (index, (path, label)) in parts.iter().enumerate() {
        if index > 0 {
            ui.label(egui::RichText::new("/").color(TEXT_DIM));
        }
        if ui.link(label).clicked() {
            world.resource_mut::<EditorAssets>().current_folder = path.clone();
        }
    }
}

fn selection_footer(world: &mut World, ui: &mut egui::Ui, selected_entities: &SelectedEntities) {
    let selection = world.resource::<EditorAssets>().selected.clone();
    let selected_embedded = selection
        .as_ref()
        .and_then(|selection| embedded_asset_for_selection(world, selection));
    if let Some(AssetSelection::SubAsset { label, kind, .. }) = selection.clone() {
        ui.add(
            egui::Label::new(format!(
                "Selected: {}: {}",
                subasset_kind_label(kind),
                label
            ))
            .truncate(),
        );
        ui.horizontal_wrapped(|ui| match kind {
            AssetSubAssetKind::Mesh => {
                if selected_embedded
                    .as_ref()
                    .is_some_and(|embedded| embedded.requires_skinned_hierarchy)
                {
                    if ui.button("Place Skinned Model").clicked() {
                        if let Some(selection) = selection.as_ref() {
                            place_subasset_operator(world, selection.clone(), Vec3::ZERO);
                        }
                    }
                } else if ui.button("Place At Origin").clicked() {
                    if let Some(selection) = selection.as_ref() {
                        place_subasset_operator(world, selection.clone(), Vec3::ZERO);
                    }
                }
            }
            AssetSubAssetKind::Texture => {
                if ui.button("Apply Texture To Selection").clicked() {
                    if let Some(asset) = texture_asset_from_subasset_selection(world, &selection) {
                        apply_texture_operator(world, asset, selected_entities);
                    }
                }
            }
            AssetSubAssetKind::Material => {
                ui.small(egui::RichText::new("Embedded source material").color(TEXT_DIM));
            }
            AssetSubAssetKind::Skeleton => {
                ui.small(egui::RichText::new("Inspect-only rig metadata").color(TEXT_DIM));
            }
            AssetSubAssetKind::AnimationClip => {
                let animated_actor = selected_entities
                    .as_slice()
                    .iter()
                    .copied()
                    .find(|entity| world.get::<shared::SkinnedMeshRenderer>(*entity).is_some());
                let action = if animated_actor.is_some() {
                    "Assign To Selected Actor"
                } else {
                    "Create Animated Actor"
                };
                if ui.button(action).clicked() {
                    if let Some(selection) = selection.as_ref() {
                        if let Some(entity) = animated_actor {
                            assign_animation_clip_operator(world, selection.clone(), entity);
                        } else {
                            place_subasset_operator(world, selection.clone(), Vec3::ZERO);
                        }
                    }
                }
            }
        });
    } else if let Some(asset) = world.resource::<EditorAssets>().selected_asset().cloned() {
        ui.add(egui::Label::new(format!("Selected: {}", asset_label(&asset))).truncate());
        ui.horizontal_wrapped(|ui| {
            if matches!(asset.kind, EditorAssetKind::Texture)
                && ui.button("Apply Texture To Selection").clicked()
            {
                apply_texture_operator(world, asset.clone(), selected_entities);
            }
            if matches!(
                asset.kind,
                EditorAssetKind::Primitive(_)
                    | EditorAssetKind::Light(_)
                    | EditorAssetKind::Model
                    | EditorAssetKind::AudioClip
                    | EditorAssetKind::Prefab
            ) && ui.button("Place At Origin").clicked()
            {
                place_asset_operator(world, asset.clone(), Vec3::ZERO);
            }
            if matches!(asset.kind, EditorAssetKind::Level) && ui.button("Open Scene").clicked() {
                open_level_asset(world, &asset);
            }
        });
    } else {
        ui.label(egui::RichText::new("No asset selected").color(TEXT_DIM));
    }
}

fn folder_tree_branch(
    world: &mut World,
    ui: &mut egui::Ui,
    folders: &[FolderSnapshot],
    node_path: &str,
    depth: usize,
    current_folder: &str,
) {
    let Some(folder) = folders.iter().find(|folder| folder.path == node_path) else {
        return;
    };
    let path = &folder.path;
    let name = &folder.name;
    let children: Vec<&FolderSnapshot> = folders
        .iter()
        .filter(|child| child.parent.as_deref() == Some(path.as_str()))
        .collect();
    let expanded = children.is_empty()
        || world
            .resource::<AssetBrowserUiState>()
            .expanded_folders
            .contains(path);

    let selected = current_folder == *path;
    ui.horizontal(|ui| {
        ui.add_space(depth as f32 * 12.0);
        if children.is_empty() {
            ui.add_sized(
                [18.0, 20.0],
                egui::Label::new(icon_text(icons::DOT_OUTLINE, 12.0).color(TEXT_DIM)),
            );
        } else {
            let icon = if expanded {
                icons::CARET_DOWN
            } else {
                icons::CARET_RIGHT
            };
            if ui
                .add_sized(
                    [18.0, 18.0],
                    egui::Button::new(icon_text(icon, 12.0)).frame(false),
                )
                .clicked()
            {
                let mut state = world.resource_mut::<AssetBrowserUiState>();
                if expanded {
                    state.expanded_folders.remove(path);
                } else {
                    state.expanded_folders.insert(path.clone());
                }
            }
        }
        ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM));
        if ui
            .add_sized(
                [fit_width(ui, 72.0, f32::INFINITY), 20.0],
                egui::Button::selectable(selected, name.as_str()),
            )
            .clicked()
        {
            world.resource_mut::<EditorAssets>().current_folder = path.clone();
        }
    });

    if expanded {
        for child in children {
            folder_tree_branch(world, ui, folders, &child.path, depth + 1, current_folder);
        }
    }
}

fn child_folders_for_content(
    folders: &[FolderSnapshot],
    current_folder: &str,
    search: &str,
) -> Vec<FolderSnapshot> {
    if !search.trim().is_empty() {
        return Vec::new();
    }
    let mut children: Vec<FolderSnapshot> = folders
        .iter()
        .filter(|folder| folder.parent.as_deref() == Some(current_folder))
        .cloned()
        .collect();
    children.sort_by(|a, b| natural_cmp(&a.name, &b.name));
    children
}

fn visible_assets(
    assets: &[AssetRow],
    current_folder: &str,
    state: &AssetBrowserStateSnapshot,
) -> Vec<AssetRow> {
    let search = state.search.trim().to_ascii_lowercase();
    let mut rows: Vec<AssetRow> = assets
        .iter()
        .filter(|row| {
            if state.recursive {
                row.asset.folder_path == current_folder
                    || row
                        .asset
                        .folder_path
                        .strip_prefix(current_folder)
                        .is_some_and(|rest| rest.starts_with('/'))
            } else {
                row.asset.folder_path == current_folder
            }
        })
        .filter(|row| asset_matches_kind_filter(&row.asset, state.kind_filter))
        .filter(|row| {
            search.is_empty()
                || row.asset.label.to_ascii_lowercase().contains(&search)
                || row
                    .asset
                    .path
                    .as_deref()
                    .unwrap_or_default()
                    .to_ascii_lowercase()
                    .contains(&search)
        })
        .cloned()
        .collect();

    rows.sort_by(|a, b| match state.sort {
        AssetSort::Name => natural_cmp(&a.asset.label, &b.asset.label),
        AssetSort::Kind => kind_label(&a.asset.kind).cmp(kind_label(&b.asset.kind)),
        AssetSort::Modified => b.modified.cmp(&a.modified),
        AssetSort::Size => b.file_size.cmp(&a.file_size),
    });
    rows
}

#[expect(
    clippy::too_many_arguments,
    reason = "asset grid rendering keeps immediate-mode UI inputs explicit"
)]
fn asset_grid(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    folders: &[FolderSnapshot],
    assets: &[AssetRow],
    cache_snapshot: &ThumbnailCacheSnapshot,
    thumbnail_size: f32,
) {
    let thumb_size = thumbnail_size.clamp(48.0, 112.0);
    let cell_width = thumb_size + 20.0;
    let gap = 6.0;
    let columns = ((ui.available_width() + gap) / (cell_width + gap))
        .floor()
        .max(1.0) as usize;

    for folder_row in folders.chunks(columns) {
        ui.horizontal(|ui| {
            ui.spacing_mut().item_spacing = egui::vec2(gap, gap);
            for folder in folder_row {
                let response = draw_folder_cell(ui, folder, thumbnail_size);
                if response.double_clicked() || response.clicked() {
                    world.resource_mut::<EditorAssets>().current_folder = folder.path.clone();
                }
            }
        });
    }

    for asset_row in assets.chunks(columns) {
        ui.horizontal(|ui| {
            ui.spacing_mut().item_spacing = egui::vec2(gap, gap);
            for row in asset_row {
                draw_asset_grid_item(
                    world,
                    ui,
                    selected_entities,
                    selected,
                    row,
                    cache_snapshot,
                    thumbnail_size,
                );
            }
        });

        for row in asset_row {
            if row.asset.path.as_deref().is_some_and(|path| {
                world
                    .resource::<AssetBrowserUiState>()
                    .expanded_assets
                    .contains(path)
            }) {
                embedded_asset_shelf(
                    world,
                    ui,
                    selected_entities,
                    selected,
                    &row.asset,
                    cache_snapshot,
                    thumbnail_size,
                );
            }
        }
    }
}

fn draw_asset_grid_item(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    row: &AssetRow,
    cache_snapshot: &ThumbnailCacheSnapshot,
    thumbnail_size: f32,
) {
    let selection = &row.selection;
    let asset = &row.asset;
    let is_selected = selected.as_ref() == Some(selection);
    let failed = cache_snapshot.is_failed(asset);
    let has_children = has_embedded_assets(world, asset);
    let response = draw_asset_cell_with(
        ui,
        asset,
        cache_snapshot.texture_for(asset),
        cache_snapshot.is_pending(asset),
        failed.then_some("failed"),
        is_selected,
        thumbnail_size,
    );

    if has_children {
        let expanded = asset.path.as_deref().is_some_and(|path| {
            world
                .resource::<AssetBrowserUiState>()
                .expanded_assets
                .contains(path)
        });
        let icon = if expanded {
            icons::CARET_DOWN
        } else {
            icons::CARET_RIGHT
        };
        let button_rect = egui::Rect::from_min_size(
            response.rect.min + egui::vec2(6.0, 6.0),
            egui::vec2(18.0, 18.0),
        );
        let expand_response = ui.interact(
            button_rect,
            egui::Id::new((
                "asset_expand",
                asset.path.as_deref().unwrap_or(&asset.label),
            )),
            egui::Sense::click(),
        );
        ui.painter().rect(
            button_rect,
            3.0,
            if expand_response.hovered() {
                ELEVATED_BG
            } else {
                WIDGET_BG.linear_multiply(1.15)
            },
            egui::Stroke::new(1.0, BORDER),
            egui::StrokeKind::Inside,
        );
        ui.painter().text(
            button_rect.center(),
            egui::Align2::CENTER_CENTER,
            icon.as_str(),
            egui::FontId::new(11.0, egui::FontFamily::Name(PHOSPHOR.into())),
            TEXT,
        );
        if expand_response.clicked() {
            if let Some(path) = asset.path.as_ref() {
                toggle_asset_expanded(world, path);
            }
        }
    }

    if failed && response.double_clicked() {
        let key = crate::assets::asset_cache_key(asset);
        world
            .resource_mut::<crate::assets::AssetThumbnailCache>()
            .retry(&key);
    }

    if response.clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(selection.clone());
    }
    if response.drag_started() {
        world
            .resource_mut::<EditorAssets>()
            .start_drag(selection.clone());
    }
    response.context_menu(|ui| {
        asset_context_menu(world, ui, asset, selected_entities);
    });
}

fn embedded_asset_shelf(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    parent_asset: &EditorAsset,
    cache_snapshot: &ThumbnailCacheSnapshot,
    thumbnail_size: f32,
) {
    let embedded = embedded_assets_for_asset(world, parent_asset);
    if embedded.is_empty() {
        return;
    }
    prefetch_embedded_thumbnails(world, parent_asset, &embedded);
    let parent_texture = cache_snapshot.texture_for(parent_asset);

    ui.add_space(4.0);
    egui::Frame::new()
        .fill(WIDGET_BG.linear_multiply(0.72))
        .stroke(egui::Stroke::new(1.0, BORDER.linear_multiply(0.85)))
        .corner_radius(egui::CornerRadius::same(4))
        .inner_margin(egui::Margin::symmetric(8, 5))
        .show(ui, |ui| {
            ui.set_min_width(ui.available_width());
            ui.horizontal(|ui| {
                ui.label(icon_text(icons::TREE_STRUCTURE, 13.0).color(TEXT_DIM));
                ui.small(
                    egui::RichText::new(format!("{} contents", parent_asset.label)).color(TEXT_DIM),
                );
            });
            egui::ScrollArea::horizontal()
                .id_salt(format!(
                    "embedded_shelf_{}",
                    parent_asset.path.as_deref().unwrap_or(&parent_asset.label)
                ))
                .max_height(thumbnail_size.clamp(48.0, 88.0) + 56.0)
                .auto_shrink([false, true])
                .show(ui, |ui| {
                    ui.horizontal(|ui| {
                        ui.spacing_mut().item_spacing = egui::vec2(7.0, 5.0);
                        for embedded_asset in &embedded {
                            draw_embedded_asset_cell(
                                world,
                                ui,
                                selected_entities,
                                selected,
                                embedded_asset,
                                cache_snapshot,
                                parent_texture,
                                thumbnail_size,
                            );
                        }
                    });
                });
        });
    ui.add_space(4.0);
}

fn prefetch_embedded_thumbnails(
    world: &mut World,
    parent_asset: &EditorAsset,
    embedded: &[EmbeddedAsset],
) {
    let Some(parent_path) = parent_asset.path.clone() else {
        return;
    };
    let asset_server = world.resource::<AssetServer>().clone();
    let requests: Vec<EmbeddedAsset> = embedded.to_vec();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        world.resource_scope(|_world, mut studio: Mut<crate::assets::ThumbnailStudio>| {
            for embedded_asset in &requests {
                match embedded_asset.kind {
                    AssetSubAssetKind::Mesh => {
                        let Some(mesh_label) = embedded_asset.mesh_label.clone() else {
                            continue;
                        };
                        cache.request_mesh_subasset(
                            embedded_asset.thumbnail_key.clone(),
                            parent_path.clone(),
                            mesh_label,
                            embedded_asset.material_label.clone(),
                            embedded_asset.requires_skinned_hierarchy,
                            &mut studio,
                        );
                    }
                    AssetSubAssetKind::Material => {
                        let Some(material_label) = embedded_asset.material_label.clone() else {
                            continue;
                        };
                        cache.request_source_material(
                            embedded_asset.thumbnail_key.clone(),
                            parent_path.clone(),
                            material_label,
                            &mut studio,
                        );
                    }
                    AssetSubAssetKind::Texture => {
                        let Some(texture_path) = embedded_asset.texture_path.clone() else {
                            continue;
                        };
                        cache.request_texture(
                            embedded_asset.thumbnail_key.clone(),
                            texture_path,
                            &asset_server,
                        );
                    }
                    AssetSubAssetKind::Skeleton | AssetSubAssetKind::AnimationClip => {}
                }
            }
        });
    });
}

#[expect(
    clippy::too_many_arguments,
    reason = "embedded asset rendering keeps immediate-mode UI inputs explicit"
)]
fn draw_embedded_asset_cell(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    embedded: &EmbeddedAsset,
    cache_snapshot: &ThumbnailCacheSnapshot,
    _parent_texture: Option<egui::TextureId>,
    thumbnail_size: f32,
) {
    let selected = selected.as_ref() == Some(&embedded.selection);
    let texture_id = cache_snapshot
        .texture_for_key(&embedded.thumbnail_key)
        .or_else(|| {
            embedded
                .texture_path
                .as_deref()
                .and_then(|path| thumbnail_for_path(world, cache_snapshot, path))
        });
    let pending = cache_snapshot.is_pending_key(&embedded.thumbnail_key);
    let failed = cache_snapshot.is_failed_key(&embedded.thumbnail_key);
    let thumb_size = thumbnail_size.clamp(44.0, 64.0);
    let cell_size = egui::vec2(104.0, thumb_size + 50.0);
    let (rect, response) = ui.allocate_exact_size(cell_size, egui::Sense::click_and_drag());
    let response = if embedded.requires_skinned_hierarchy {
        response.on_hover_text(
            "Skinned primitives place through the dedicated renderer so their joint hierarchy remains intact.",
        )
    } else {
        response
    };
    let fill = if selected {
        crate::ui::theme::SELECTION_BG_MUTED
    } else if response.hovered() {
        WIDGET_BG.linear_multiply(1.2)
    } else {
        WIDGET_BG
    };
    ui.painter().rect(
        rect,
        4.0,
        fill,
        egui::Stroke::new(1.0, if selected { ACCENT } else { BORDER }),
        egui::StrokeKind::Inside,
    );
    let thumb_rect = egui::Rect::from_min_size(
        egui::pos2(rect.center().x - thumb_size * 0.5, rect.min.y + 8.0),
        egui::vec2(thumb_size, thumb_size),
    );
    if let Some(texture_id) = texture_id {
        ui.painter().image(
            texture_id,
            thumb_rect,
            egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
            egui::Color32::WHITE,
        );
    } else {
        ui.painter().rect(
            thumb_rect,
            4.0,
            ELEVATED_BG.linear_multiply(0.6),
            egui::Stroke::new(1.0, BORDER),
            egui::StrokeKind::Inside,
        );
        let icon = if pending {
            icons::CIRCLE_NOTCH
        } else if failed {
            icons::WARNING_CIRCLE
        } else {
            subasset_icon(embedded.kind)
        };
        ui.painter().text(
            thumb_rect.center(),
            egui::Align2::CENTER_CENTER,
            icon.as_str(),
            egui::FontId::new(24.0, egui::FontFamily::Name(PHOSPHOR.into())),
            TEXT,
        );
    }

    let label = compact_subasset_label(embedded);
    let detail = compact_text(&embedded.detail, 18);
    let label_pos = egui::pos2(rect.center().x, thumb_rect.max.y + 7.0);
    ui.painter().text(
        label_pos,
        egui::Align2::CENTER_TOP,
        label.as_str(),
        egui::FontId::new(12.0, egui::FontFamily::Proportional),
        if selected {
            crate::ui::theme::TEXT_SELECTED
        } else {
            TEXT
        },
    );
    ui.painter().text(
        label_pos + egui::vec2(0.0, 17.0),
        egui::Align2::CENTER_TOP,
        format!("{} | {}", subasset_kind_label(embedded.kind), detail),
        egui::FontId::new(10.0, egui::FontFamily::Proportional),
        TEXT_DIM,
    );

    if response.clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(embedded.selection.clone());
    }
    if response.drag_started() && embedded.kind != AssetSubAssetKind::Skeleton {
        world
            .resource_mut::<EditorAssets>()
            .start_drag(embedded.selection.clone());
    }
    response.context_menu(|ui| {
        subasset_context_menu(world, ui, embedded, selected_entities);
    });
}

fn compact_subasset_label(embedded: &EmbeddedAsset) -> String {
    let base = match embedded.kind {
        AssetSubAssetKind::Mesh => embedded
            .label
            .split_once(" / ")
            .map(|(head, _)| head)
            .unwrap_or(&embedded.label),
        AssetSubAssetKind::Material
        | AssetSubAssetKind::Texture
        | AssetSubAssetKind::Skeleton
        | AssetSubAssetKind::AnimationClip => embedded.label.as_str(),
    };
    compact_text(base, 16)
}

fn compact_text(text: &str, max_chars: usize) -> String {
    let mut chars = text.chars();
    let mut compact = String::new();
    for _ in 0..max_chars {
        let Some(ch) = chars.next() else {
            return text.to_string();
        };
        compact.push(ch);
    }
    if chars.next().is_some() {
        compact.push_str("...");
    }
    compact
}

fn thumbnail_for_path(
    world: &World,
    cache_snapshot: &ThumbnailCacheSnapshot,
    path: &str,
) -> Option<egui::TextureId> {
    let assets = world.resource::<EditorAssets>();
    assets
        .assets
        .iter()
        .find(|asset| asset.path.as_deref() == Some(path))
        .and_then(|asset| cache_snapshot.texture_for(asset))
}

fn toggle_asset_expanded(world: &mut World, path: &str) {
    let mut state = world.resource_mut::<AssetBrowserUiState>();
    if !state.expanded_assets.insert(path.to_string()) {
        state.expanded_assets.remove(path);
    }
}

fn subasset_icon(kind: AssetSubAssetKind) -> Icon {
    match kind {
        AssetSubAssetKind::Mesh => icons::CUBE,
        AssetSubAssetKind::Material => icons::PALETTE,
        AssetSubAssetKind::Texture => icons::IMAGE,
        AssetSubAssetKind::Skeleton => icons::BONE,
        AssetSubAssetKind::AnimationClip => icons::FILM_STRIP,
    }
}

fn subasset_kind_label(kind: AssetSubAssetKind) -> &'static str {
    match kind {
        AssetSubAssetKind::Mesh => "Mesh",
        AssetSubAssetKind::Material => "Material",
        AssetSubAssetKind::Texture => "Texture",
        AssetSubAssetKind::Skeleton => "Skeleton",
        AssetSubAssetKind::AnimationClip => "Animation Clip",
    }
}

fn asset_list(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    folders: &[FolderSnapshot],
    assets: &[AssetRow],
    cache_snapshot: &ThumbnailCacheSnapshot,
) {
    if ui.available_width() < COMPACT_LIST_WIDTH {
        compact_asset_list(
            world,
            ui,
            selected_entities,
            selected,
            folders,
            assets,
            cache_snapshot,
        );
        return;
    }

    ui.horizontal(|ui| {
        ui.add_sized([236.0, 18.0], egui::Label::new("Name"));
        ui.add_sized([80.0, 18.0], egui::Label::new("Type"));
        ui.add_sized([72.0, 18.0], egui::Label::new("Size"));
        ui.add_sized([88.0, 18.0], egui::Label::new("Modified"));
        ui.label("Path");
    });

    for folder in folders {
        let response = egui::Frame::new()
            .fill(WIDGET_BG.linear_multiply(0.9))
            .inner_margin(egui::Margin::symmetric(6, 3))
            .show(ui, |ui| {
                ui.horizontal(|ui| {
                    ui.add_sized([20.0, 20.0], egui::Label::new(""));
                    ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM));
                    ui.add_sized(
                        [196.0, 20.0],
                        egui::Button::selectable(false, folder.name.as_str()),
                    )
                })
                .inner
            })
            .inner;
        if response.clicked() {
            world.resource_mut::<EditorAssets>().current_folder = folder.path.clone();
        }
    }

    for row in assets {
        draw_asset_list_item(world, ui, selected_entities, selected, row);
        if row.asset.path.as_deref().is_some_and(|path| {
            world
                .resource::<AssetBrowserUiState>()
                .expanded_assets
                .contains(path)
        }) {
            embedded_asset_shelf(
                world,
                ui,
                selected_entities,
                selected,
                &row.asset,
                cache_snapshot,
                56.0,
            );
        }
    }
}

fn draw_asset_list_item(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    row: &AssetRow,
) {
    let has_children = has_embedded_assets(world, &row.asset);
    let expanded = row.asset.path.as_deref().is_some_and(|path| {
        world
            .resource::<AssetBrowserUiState>()
            .expanded_assets
            .contains(path)
    });
    let is_selected = selected.as_ref() == Some(&row.selection);
    let response = egui::Frame::new()
        .fill(if is_selected {
            crate::ui::theme::SELECTION_BG_MUTED
        } else {
            WIDGET_BG.linear_multiply(0.9)
        })
        .stroke(egui::Stroke::new(
            1.0,
            if is_selected { ACCENT } else { BORDER },
        ))
        .inner_margin(egui::Margin::symmetric(6, 3))
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                if has_children {
                    let icon = if expanded {
                        icons::CARET_DOWN
                    } else {
                        icons::CARET_RIGHT
                    };
                    if ui
                        .add_sized(
                            [20.0, 20.0],
                            egui::Button::new(icon_text(icon, 12.0)).frame(false),
                        )
                        .clicked()
                    {
                        if let Some(path) = row.asset.path.as_ref() {
                            toggle_asset_expanded(world, path);
                        }
                    }
                } else {
                    ui.add_sized([20.0, 20.0], egui::Label::new(""));
                }
                ui.label(icon_text(kind_icon(&row.asset.kind), 14.0).color(TEXT_DIM));
                let response = ui.add_sized(
                    [196.0, 20.0],
                    egui::Button::selectable(is_selected, row.asset.label.as_str()),
                );
                ui.add_sized([80.0, 20.0], egui::Label::new(kind_label(&row.asset.kind)));
                ui.add_sized(
                    [72.0, 20.0],
                    egui::Label::new(
                        row.file_size
                            .map(format_bytes)
                            .unwrap_or_else(|| "-".to_string()),
                    ),
                );
                ui.add_sized(
                    [88.0, 20.0],
                    egui::Label::new(
                        row.modified
                            .map(format_modified)
                            .unwrap_or_else(|| "-".to_string()),
                    ),
                );
                ui.add(
                    egui::Label::new(row.asset.path.as_deref().unwrap_or("Built-in")).truncate(),
                );
                ui.menu_button(icon_text(icons::DOTS_THREE_VERTICAL, 13.0), |ui| {
                    asset_context_menu(world, ui, &row.asset, selected_entities);
                });
                response
            })
            .inner
        })
        .inner;

    if response.clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(row.selection.clone());
    }
    if response.drag_started() {
        world
            .resource_mut::<EditorAssets>()
            .start_drag(row.selection.clone());
    }
    response.context_menu(|ui| {
        asset_context_menu(world, ui, &row.asset, selected_entities);
    });
}

fn compact_asset_list(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selected: &Option<AssetSelection>,
    folders: &[FolderSnapshot],
    assets: &[AssetRow],
    cache_snapshot: &ThumbnailCacheSnapshot,
) {
    for folder in folders {
        let mut clicked = false;
        ui.horizontal(|ui| {
            ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM));
            clicked = ui
                .add_sized(
                    [fit_width(ui, 120.0, f32::INFINITY), 22.0],
                    egui::Button::selectable(false, folder.name.as_str()),
                )
                .clicked();
        });
        if clicked {
            world.resource_mut::<EditorAssets>().current_folder = folder.path.clone();
        }
        ui.small(egui::RichText::new("Folder").color(TEXT_DIM));
        ui.add_space(4.0);
    }

    for row in assets {
        let is_selected = selected.as_ref() == Some(&row.selection);
        let has_children = has_embedded_assets(world, &row.asset);
        let expanded = row.asset.path.as_deref().is_some_and(|path| {
            world
                .resource::<AssetBrowserUiState>()
                .expanded_assets
                .contains(path)
        });
        let mut response = None;
        ui.horizontal(|ui| {
            if has_children {
                let icon = if expanded {
                    icons::CARET_DOWN
                } else {
                    icons::CARET_RIGHT
                };
                if ui
                    .add_sized(
                        [20.0, 20.0],
                        egui::Button::new(icon_text(icon, 12.0)).frame(false),
                    )
                    .clicked()
                {
                    if let Some(path) = row.asset.path.as_ref() {
                        toggle_asset_expanded(world, path);
                    }
                }
            }
            ui.label(icon_text(kind_icon(&row.asset.kind), 14.0).color(TEXT_DIM));
            response = Some(ui.add_sized(
                [fit_width(ui, 120.0, f32::INFINITY), 22.0],
                egui::Button::selectable(is_selected, row.asset.label.as_str()),
            ));
            ui.menu_button(icon_text(icons::DOTS_THREE_VERTICAL, 13.0), |ui| {
                asset_context_menu(world, ui, &row.asset, selected_entities);
            });
        });
        let response = response.expect("compact asset list response");
        let size = row
            .file_size
            .map(format_bytes)
            .unwrap_or_else(|| "-".to_string());
        let modified = row
            .modified
            .map(format_modified)
            .unwrap_or_else(|| "-".to_string());
        let path = row.asset.path.as_deref().unwrap_or("Built-in");
        ui.add(
            egui::Label::new(
                egui::RichText::new(format!(
                    "{} | {} | {} | {}",
                    kind_label(&row.asset.kind),
                    size,
                    modified,
                    path
                ))
                .color(TEXT_DIM),
            )
            .truncate(),
        );
        ui.add_space(4.0);

        if response.clicked() {
            world
                .resource_mut::<EditorAssets>()
                .select(row.selection.clone());
        }
        if response.drag_started() {
            world
                .resource_mut::<EditorAssets>()
                .start_drag(row.selection.clone());
        }
        response.context_menu(|ui| {
            asset_context_menu(world, ui, &row.asset, selected_entities);
        });
        if expanded {
            embedded_asset_shelf(
                world,
                ui,
                selected_entities,
                selected,
                &row.asset,
                cache_snapshot,
                56.0,
            );
        }
    }
}

fn draw_folder_cell(
    ui: &mut egui::Ui,
    folder: &FolderSnapshot,
    thumbnail_size: f32,
) -> egui::Response {
    let thumb_size = thumbnail_size.clamp(48.0, 112.0);
    let cell_size = egui::vec2(thumb_size + 20.0, thumb_size + 30.0);
    let (rect, response) = ui.allocate_exact_size(cell_size, egui::Sense::click());
    let fill = if response.hovered() {
        ELEVATED_BG
    } else {
        WIDGET_BG
    };
    ui.painter().rect(
        rect,
        4.0,
        fill,
        egui::Stroke::new(1.0, BORDER),
        egui::StrokeKind::Inside,
    );
    ui.painter().text(
        rect.center_top() + egui::vec2(0.0, 10.0 + thumb_size * 0.45),
        egui::Align2::CENTER_CENTER,
        icons::FOLDER_SIMPLE.as_str(),
        egui::FontId::new(
            (thumb_size * 0.48).max(24.0),
            egui::FontFamily::Name("phosphor-regular".into()),
        ),
        TEXT,
    );
    ui.painter().text(
        egui::pos2(rect.center().x, rect.max.y - 6.0),
        egui::Align2::CENTER_BOTTOM,
        folder.name.as_str(),
        egui::FontId::new(11.0, egui::FontFamily::Proportional),
        TEXT,
    );
    response
}

fn details_panel(world: &mut World, ui: &mut egui::Ui, selected_entities: &SelectedEntities) {
    ui.label(panel_heading("Details"));
    ui.separator();
    let selection = world.resource::<EditorAssets>().selected.clone();
    if let Some(selection @ AssetSelection::SubAsset { .. }) = selection {
        subasset_details_panel(world, ui, selected_entities, &selection);
    } else if let Some(asset) = world.resource::<EditorAssets>().selected_asset().cloned() {
        top_level_asset_details_panel(world, ui, selected_entities, &asset);
    } else {
        ui.label(egui::RichText::new("No asset selected").color(TEXT_DIM));
    }
}

pub(crate) fn top_level_asset_details_panel(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    asset: &EditorAsset,
) {
    asset_details_header(world, ui, asset);
    ui.add_space(8.0);
    detail_row(ui, "Path", asset.path.as_deref().unwrap_or("Built-in"));
    detail_row(ui, "Folder", asset.folder_path.as_str());

    if let Some(path) = asset.path.as_deref() {
        if let Some(record) = world
            .get_resource::<crate::asset_db::AssetRegistry>()
            .and_then(|registry| find_asset_by_path(registry, path))
        {
            detail_row(ui, "Asset ID", &record.id.as_string());
            if matches!(asset.kind, EditorAssetKind::Model) {
                ui.separator();
                ui.label(panel_heading("Import Settings"));
                import_settings_editor(world, ui, asset, path, &record.import_settings);
                if let Some(manifest) = record.import_settings.static_mesh_manifest_path.as_deref()
                {
                    detail_row(ui, "Static mesh artifact", manifest);
                }
            }
            if !record.dependencies.is_empty() {
                ui.separator();
                ui.label(panel_heading("Dependencies"));
                for dep in &record.dependencies {
                    ui.add(egui::Label::new(dep).truncate());
                }
            }
        }
        detail_row(
            ui,
            "Size",
            file_size(path).map(format_bytes).as_deref().unwrap_or("-"),
        );
        detail_row(
            ui,
            "Modified",
            modified_time(path)
                .map(format_modified)
                .as_deref()
                .unwrap_or("-"),
        );
        if matches!(asset.kind, EditorAssetKind::Material) {
            ui.separator();
            ui.label(panel_heading("Material"));
            if MaterialInstanceAsset::load_from_path(path).is_ok() {
                material_instance_asset_editor(world, ui, asset, path);
            } else {
                material_asset_editor(world, ui, asset, path);
            }
        }
        if matches!(asset.kind, EditorAssetKind::AudioClip) {
            detail_row(ui, "Format", audio_format_label(path));
            let previewing = world
                .resource::<crate::play::audio_preview::AudioPreviewState>()
                .clip_path
                .as_deref()
                == Some(path);
            ui.horizontal_wrapped(|ui| {
                if ui
                    .add_enabled(
                        !previewing,
                        egui::Button::new(format!("{} Audition", icons::PLAY.as_str())),
                    )
                    .clicked()
                {
                    if let Err(error) =
                        crate::play::audio_preview::audition_audio_asset(world, asset)
                    {
                        world.resource_mut::<SceneIo>().status =
                            format!("Audio audition failed: {error}");
                    }
                }
                if ui
                    .add_enabled(
                        previewing,
                        egui::Button::new(format!("{} Stop", icons::STOP.as_str())),
                    )
                    .clicked()
                {
                    crate::play::audio_preview::stop_audio_preview(world);
                }
            });
        }
    }

    ui.separator();
    asset_action_buttons(world, ui, selected_entities, asset);
}

fn asset_details_header(world: &World, ui: &mut egui::Ui, asset: &EditorAsset) {
    ui.horizontal_wrapped(|ui| {
        ui.label(
            egui::RichText::new(kind_icon(&asset.kind).as_str())
                .font(egui::FontId::new(
                    30.0,
                    egui::FontFamily::Name(PHOSPHOR.into()),
                ))
                .color(ACCENT),
        );
        ui.vertical(|ui| {
            ui.strong(asset.label.as_str());
            ui.small(egui::RichText::new(kind_label(&asset.kind)).color(TEXT_DIM));
            if let (Some(state), Some(path)) = (
                world.get_resource::<CollaborationState>(),
                asset.path.as_deref(),
            ) {
                let status = state.file_status(Path::new(path));
                let _ = file_status_indicator_ui(ui, &status, Path::new(path));
            }
        });
    });
}

fn subasset_details_panel(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    selection: &AssetSelection,
) {
    let Some(embedded) = embedded_asset_for_selection(world, selection) else {
        ui.colored_label(egui::Color32::YELLOW, "Embedded asset is not available.");
        return;
    };
    let AssetSelection::SubAsset {
        parent_path,
        sub_asset_id,
        ..
    } = selection
    else {
        return;
    };
    ui.horizontal_wrapped(|ui| {
        ui.label(
            egui::RichText::new(subasset_icon(embedded.kind).as_str())
                .font(egui::FontId::new(
                    30.0,
                    egui::FontFamily::Name(PHOSPHOR.into()),
                ))
                .color(ACCENT),
        );
        ui.vertical(|ui| {
            ui.strong(embedded.label.as_str());
            ui.small(egui::RichText::new(subasset_kind_label(embedded.kind)).color(TEXT_DIM));
        });
    });
    ui.add_space(8.0);
    detail_row(ui, "Parent", parent_path);
    detail_row(ui, "ID", sub_asset_id);
    detail_row(ui, "Source", &embedded.detail);

    ui.separator();
    match embedded.kind {
        AssetSubAssetKind::Mesh => {
            if embedded.requires_skinned_hierarchy {
                if ui.button("Place Skinned Model At Origin").clicked() {
                    place_subasset_operator(world, selection.clone(), Vec3::ZERO);
                }
            } else if ui.button("Place At Origin").clicked() {
                place_subasset_operator(world, selection.clone(), Vec3::ZERO);
            }
        }
        AssetSubAssetKind::Texture => {
            if ui.button("Apply Texture To Selection").clicked() {
                if let Some(asset) =
                    texture_asset_from_subasset_selection(world, &Some(selection.clone()))
                {
                    apply_texture_operator(world, asset, selected_entities);
                }
            }
        }
        AssetSubAssetKind::Material => {
            ui.small(
                egui::RichText::new(
                    "Source materials are assigned through static mesh renderer slots.",
                )
                .color(TEXT_DIM),
            );
        }
        AssetSubAssetKind::Skeleton => {
            ui.small(
                egui::RichText::new("Skeleton metadata is generated from the model source.")
                    .color(TEXT_DIM),
            );
        }
        AssetSubAssetKind::AnimationClip => {
            let animated_actor = selected_entities
                .as_slice()
                .iter()
                .copied()
                .find(|entity| world.get::<shared::SkinnedMeshRenderer>(*entity).is_some());
            let action = if animated_actor.is_some() {
                "Assign To Selected Actor"
            } else {
                "Create Animated Actor"
            };
            if ui.button(action).clicked() {
                if let Some(entity) = animated_actor {
                    assign_animation_clip_operator(world, selection.clone(), entity);
                } else {
                    place_subasset_operator(world, selection.clone(), Vec3::ZERO);
                }
            }
        }
    }
    if ui.button("Select Parent Asset").clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(AssetSelection::File(parent_path.clone()));
    }
}

fn asset_action_buttons(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &SelectedEntities,
    asset: &EditorAsset,
) {
    ui.horizontal_wrapped(|ui| {
        if matches!(asset.kind, EditorAssetKind::Texture) && ui.button("Apply Texture").clicked() {
            apply_texture_operator(world, asset.clone(), selected_entities);
        }
        if matches!(asset.kind, EditorAssetKind::Material) && ui.button("Apply Material").clicked()
        {
            apply_material_operator(world, asset.clone(), selected_entities);
        }
        if matches!(
            asset.kind,
            EditorAssetKind::Primitive(_)
                | EditorAssetKind::Light(_)
                | EditorAssetKind::Model
                | EditorAssetKind::AudioClip
                | EditorAssetKind::Prefab
        ) && ui.button("Place At Origin").clicked()
        {
            place_asset_operator(world, asset.clone(), Vec3::ZERO);
        }
        if matches!(asset.kind, EditorAssetKind::Level) && ui.button("Open Scene").clicked() {
            open_level_asset(world, asset);
        }
        if matches!(asset.kind, EditorAssetKind::Model)
            && has_embedded_assets(world, asset)
            && ui.button("Toggle Contents").clicked()
        {
            if let Some(path) = asset.path.as_ref() {
                toggle_asset_expanded(world, path);
            }
        }
        if matches!(asset.kind, EditorAssetKind::Model) && ui.button("Reimport").clicked() {
            reimport_asset(world, asset);
        }
        if asset.path.is_some() && ui.button("Move To Trash").clicked() {
            request_delete_for_asset(world, asset);
        }
    });
}

fn open_level_asset(world: &mut World, asset: &EditorAsset) {
    let Some(path) = asset.path.as_deref() else {
        return;
    };
    world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::OpenPath(PathBuf::from(path)));
}

fn import_settings_editor(
    world: &mut World,
    ui: &mut egui::Ui,
    asset: &EditorAsset,
    path: &str,
    current: &ImportSettings,
) {
    let animation_clip_options = current
        .animation_manifest_path
        .as_deref()
        .and_then(|manifest_path| load_animation_manifest(manifest_path).ok())
        .map(|manifest| {
            manifest
                .clips
                .into_iter()
                .map(|clip| (clip.id, clip.label))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        let reset = state
            .import_draft
            .as_ref()
            .is_none_or(|draft| draft.path != path);
        if reset {
            state.import_draft = Some(ImportSettingsDraft {
                path: path.to_string(),
                settings: current.clone(),
            });
        }
    }

    let mut apply = None;
    let mut revert = false;
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        let Some(draft) = state.import_draft.as_mut() else {
            return;
        };
        ui.add(egui::Slider::new(&mut draft.settings.scale, 0.01..=10.0).text("Scale"));
        ui.checkbox(&mut draft.settings.generate_collider, "Generate collider");
        ui.checkbox(&mut draft.settings.lod0_only, "LOD0 only");
        egui::ComboBox::from_id_salt("model_placement_mode")
            .selected_text(match draft.settings.placement_mode {
                ModelPlacementMode::StaticAsset => "Renderable Asset (Auto)",
                ModelPlacementMode::SceneInstance => "Scene Instance",
            })
            .show_ui(ui, |ui| {
                ui.selectable_value(
                    &mut draft.settings.placement_mode,
                    ModelPlacementMode::StaticAsset,
                    "Renderable Asset (Auto)",
                );
                ui.selectable_value(
                    &mut draft.settings.placement_mode,
                    ModelPlacementMode::SceneInstance,
                    "Scene Instance",
                );
            });
        egui::ComboBox::from_id_salt("model_hierarchy_mode")
            .selected_text(match draft.settings.hierarchy_mode {
                ModelHierarchyMode::SingleActor => "One Actor",
                ModelHierarchyMode::SourceHierarchy => "Source Hierarchy",
            })
            .show_ui(ui, |ui| {
                ui.selectable_value(
                    &mut draft.settings.hierarchy_mode,
                    ModelHierarchyMode::SingleActor,
                    "One Actor",
                );
                ui.selectable_value(
                    &mut draft.settings.hierarchy_mode,
                    ModelHierarchyMode::SourceHierarchy,
                    "Source Hierarchy",
                );
            });
        egui::ComboBox::from_id_salt("model_material_policy")
            .selected_text(match draft.settings.material_policy {
                MaterialImportPolicy::SourceMaterials => "Source Materials",
                MaterialImportPolicy::AuthoringOverride => "Authoring Override",
            })
            .show_ui(ui, |ui| {
                ui.selectable_value(
                    &mut draft.settings.material_policy,
                    MaterialImportPolicy::SourceMaterials,
                    "Source Materials",
                );
                ui.selectable_value(
                    &mut draft.settings.material_policy,
                    MaterialImportPolicy::AuthoringOverride,
                    "Authoring Override",
                );
            });
        if !animation_clip_options.is_empty() {
            let selected_default = draft
                .settings
                .default_animation_clip_id
                .as_deref()
                .and_then(|id| {
                    animation_clip_options
                        .iter()
                        .find(|(clip_id, _)| clip_id == id)
                        .map(|(_, label)| label.as_str())
                })
                .unwrap_or_else(|| {
                    if draft.settings.default_animation_clip_id.is_some() {
                        "Missing clip"
                    } else {
                        "Imported rest pose"
                    }
                });
            egui::ComboBox::from_id_salt("model_default_animation_clip")
                .selected_text(selected_default)
                .show_ui(ui, |ui| {
                    ui.selectable_value(
                        &mut draft.settings.default_animation_clip_id,
                        None,
                        "Imported rest pose",
                    );
                    for (clip_id, label) in &animation_clip_options {
                        ui.selectable_value(
                            &mut draft.settings.default_animation_clip_id,
                            Some(clip_id.clone()),
                            label,
                        );
                    }
                });
            ui.small("Default animation is sampled and paused in edit mode; no clip is guessed.");
        }
        ui.horizontal(|ui| {
            if ui.button("Apply").clicked() {
                apply = Some(draft.settings.clone());
            }
            if ui.button("Revert").clicked() {
                revert = true;
            }
        });
    }

    if revert {
        world.resource_mut::<AssetBrowserUiState>().import_draft = Some(ImportSettingsDraft {
            path: path.to_string(),
            settings: current.clone(),
        });
    }
    if let Some(settings) = apply {
        apply_import_settings(world, asset, path, settings);
    }
}

fn apply_import_settings(
    world: &mut World,
    asset: &EditorAsset,
    path: &str,
    settings: ImportSettings,
) {
    let mut status = format!("Updated import settings for {}", asset.label);
    if let Some(mut registry) = world.get_resource_mut::<crate::asset_db::AssetRegistry>() {
        update_import_settings(&mut registry, path, settings);
        if matches!(asset.kind, EditorAssetKind::Model) {
            if let Some(record) = find_asset_mut_by_path(&mut registry, path) {
                if let Err(error) = refresh_model_artifacts(record) {
                    status = format!("Model artifact refresh failed for {}: {error}", asset.label);
                    warn!("{status}");
                }
            }
        }
        if let Err(error) = save_registry(&registry) {
            status = format!("Asset registry save failed: {error}");
            warn!("{status}");
        } else {
            registry.index_dirty = false;
        }
    }
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = status;
}

fn material_asset_editor(world: &mut World, ui: &mut egui::Ui, asset: &EditorAsset, path: &str) {
    ensure_material_draft(world, path, &asset.label);
    let shader_schemas = shader_schema_assets(world);
    let texture_assets = material_texture_candidates(world);
    let texture_drop_candidate = world
        .resource::<EditorAssets>()
        .dragging_selection()
        .and_then(|selection| {
            texture_assets
                .iter()
                .find(|candidate| candidate.selection == *selection)
        })
        .cloned();
    let mut apply = false;
    let mut revert = false;
    let mut create_instance = false;
    let mut accepted_texture_drop = false;
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        let Some(draft) = state.material_draft.as_mut() else {
            return;
        };
        if let Some(error) = draft.error.as_ref() {
            ui.colored_label(egui::Color32::YELLOW, error);
        }
        compact_text_edit(ui, "Label", &mut draft.asset.label);
        material_shader_interface(
            ui,
            &mut draft.asset,
            &shader_schemas,
            &texture_assets,
            texture_drop_candidate.as_ref(),
            &mut accepted_texture_drop,
        );
        ui.separator();
        ui.label(panel_heading("Render State"));
        ui.horizontal(|ui| {
            ui.add_sized([92.0, 20.0], egui::Label::new("Alpha mode"));
            egui::ComboBox::from_id_salt("material_alpha_mode")
                .selected_text(match draft.asset.render_state.alpha_mode {
                    MaterialAlphaMode::Opaque => "Opaque",
                    MaterialAlphaMode::Cutout => "Cutout",
                })
                .show_ui(ui, |ui| {
                    ui.selectable_value(
                        &mut draft.asset.render_state.alpha_mode,
                        MaterialAlphaMode::Opaque,
                        "Opaque",
                    );
                    ui.selectable_value(
                        &mut draft.asset.render_state.alpha_mode,
                        MaterialAlphaMode::Cutout,
                        "Cutout",
                    );
                });
        });
        if draft.asset.render_state.alpha_mode == MaterialAlphaMode::Cutout {
            ui.add(
                egui::Slider::new(&mut draft.asset.render_state.alpha_cutoff, 0.0..=1.0)
                    .text("Alpha cutoff"),
            );
        }
        ui.checkbox(&mut draft.asset.render_state.double_sided, "Double sided");
        ui.separator();
        ui.label(panel_heading("Standard Surface"));
        color_desc_edit(ui, "Base color", &mut draft.asset.material.base_color);
        ui.add(egui::Slider::new(&mut draft.asset.material.metallic, 0.0..=1.0).text("Metallic"));
        ui.add(egui::Slider::new(&mut draft.asset.material.roughness, 0.0..=1.0).text("Roughness"));
        color_desc_edit(ui, "Emissive", &mut draft.asset.material.emissive_color);
        ui.add(
            egui::Slider::new(&mut draft.asset.material.emissive_intensity, 0.0..=50_000.0)
                .text("Emissive nits"),
        );
        accepted_texture_drop |= material_texture_path_ui(
            ui,
            "Base texture",
            "standard_base_color",
            &mut draft.asset.material.base_color_texture,
            &texture_assets,
            texture_drop_candidate.as_ref(),
        );
        accepted_texture_drop |= material_texture_path_ui(
            ui,
            "Emissive texture",
            "standard_emissive",
            &mut draft.asset.material.emissive_texture,
            &texture_assets,
            texture_drop_candidate.as_ref(),
        );
        accepted_texture_drop |= material_texture_path_ui(
            ui,
            "Normal map",
            "standard_normal_map",
            &mut draft.asset.material.normal_map_texture,
            &texture_assets,
            texture_drop_candidate.as_ref(),
        );
        accepted_texture_drop |= material_texture_path_ui(
            ui,
            "Metal/rough",
            "standard_metallic_roughness",
            &mut draft.asset.material.metallic_roughness_texture,
            &texture_assets,
            texture_drop_candidate.as_ref(),
        );
        ui.horizontal(|ui| {
            if ui.button("Apply").clicked() {
                apply = true;
            }
            if ui.button("Revert").clicked() {
                revert = true;
            }
            if ui.button("Create Instance").clicked() {
                create_instance = true;
            }
        });
    }

    if revert {
        world.resource_mut::<AssetBrowserUiState>().material_draft = None;
        ensure_material_draft(world, path, &asset.label);
    }
    if apply {
        save_material_draft(world);
    }
    if create_instance {
        create_material_instance_from_base(world, path, &asset.label);
    }
    if accepted_texture_drop {
        world.resource_mut::<EditorAssets>().clear_drag();
    }
}

fn ensure_material_draft(world: &mut World, path: &str, fallback_label: &str) {
    let needs_load = world
        .resource::<AssetBrowserUiState>()
        .material_draft
        .as_ref()
        .is_none_or(|draft| draft.path != path);
    if !needs_load {
        return;
    }
    let loaded = fs::read_to_string(path)
        .map_err(|error| format!("could not read {path}: {error}"))
        .and_then(|text| {
            let snapshot = FileSnapshot::from_loaded_bytes(Path::new(path), text.as_bytes());
            ron::from_str::<MaterialAsset>(&text)
                .map(|asset| (asset, snapshot))
                .map_err(|error| format!("invalid material RON in {path}: {error}"))
        });
    let draft = match loaded {
        Ok((asset, disk_snapshot)) => MaterialAssetDraft {
            path: path.to_string(),
            asset,
            disk_snapshot,
            error: None,
        },
        Err(error) => MaterialAssetDraft {
            path: path.to_string(),
            asset: MaterialAsset {
                schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
                label: fallback_label.to_string(),
                shader: None,
                shader_ref: None,
                render_state: shared::MaterialRenderState::default(),
                material: MaterialDesc::default(),
            },
            disk_snapshot: FileSnapshot::capture(Path::new(path))
                .unwrap_or_else(|_| FileSnapshot::missing()),
            error: Some(error),
        },
    };
    world.resource_mut::<AssetBrowserUiState>().material_draft = Some(draft);
}

fn save_material_draft(world: &mut World) {
    let Some(draft) = world
        .resource::<AssetBrowserUiState>()
        .material_draft
        .clone()
    else {
        return;
    };
    let result = ron::ser::to_string_pretty(&draft.asset, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize material {}: {error}", draft.path))
        .and_then(|text| {
            publish_authored_file(
                world,
                Path::new(&draft.path),
                text.as_bytes(),
                &draft.disk_snapshot,
                FileWriteIntent::Material,
            )
        });
    match result {
        Ok(disk_snapshot) => {
            if let Some(current) = world
                .resource_mut::<AssetBrowserUiState>()
                .material_draft
                .as_mut()
            {
                current.error = None;
                current.disk_snapshot = disk_snapshot;
            }
            world.resource_mut::<EditorAssets>().refresh();
            invalidate_on_catalog_refresh(world);
            world.resource_mut::<SceneIo>().status = format!("Saved material {}", draft.path);
        }
        Err(error) => {
            if let Some(current) = world
                .resource_mut::<AssetBrowserUiState>()
                .material_draft
                .as_mut()
            {
                current.error = Some(error.clone());
            }
            world.resource_mut::<SceneIo>().status = error;
        }
    }
}

pub(crate) fn create_material_instance_from_base(
    world: &mut World,
    base_path: &str,
    base_label: &str,
) {
    let base_reference = world
        .get_resource_mut::<crate::asset_db::AssetRegistry>()
        .ok_or_else(|| "Asset registry is unavailable".to_string())
        .and_then(|mut registry| {
            ensure_asset_record(
                &mut registry,
                base_path.to_string(),
                base_label.to_string(),
                "Material",
            )
            .map(|record| {
                MaterialRef::new(
                    EditorAssetRef::new(
                        record.id.as_string(),
                        "material:source",
                        base_label.to_string(),
                    )
                    .with_source_path(base_path),
                )
            })
        });
    let base_reference = match base_reference {
        Ok(reference) => reference,
        Err(error) => {
            world.resource_mut::<SceneIo>().status = error;
            return;
        }
    };
    let base = Path::new(base_path);
    let parent = base
        .parent()
        .unwrap_or_else(|| Path::new("assets/materials"));
    let stem = base
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or("material");
    let mut index = 1usize;
    let path = loop {
        let suffix = if index == 1 {
            "instance".to_string()
        } else {
            format!("instance_{index}")
        };
        let candidate = parent.join(format!("{stem}_{suffix}.ron"));
        if !candidate.exists() {
            break candidate;
        }
        index += 1;
    };
    let instance = MaterialInstanceAsset {
        schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
        label: format!("{base_label} Instance"),
        base: base_reference,
        parameters: Vec::new(),
        textures: Vec::new(),
    };
    let result = ron::ser::to_string_pretty(&instance, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize material instance: {error}"))
        .and_then(|text| {
            publish_authored_file(
                world,
                &path,
                text.as_bytes(),
                &FileSnapshot::missing(),
                FileWriteIntent::MaterialInstance,
            )
        });
    if let Err(error) = result {
        world.resource_mut::<SceneIo>().status = error;
        return;
    }
    let normalized = path.to_string_lossy().replace('\\', "/");
    let registry_error =
        if let Some(mut registry) = world.get_resource_mut::<crate::asset_db::AssetRegistry>() {
            ensure_asset_record(
                &mut registry,
                normalized.clone(),
                instance.label.clone(),
                "Material",
            )
            .map(|_| ())
            .and_then(|_| save_registry(&registry))
            .err()
        } else {
            Some("Asset registry is unavailable".to_string())
        };
    if let Some(error) = registry_error {
        world.resource_mut::<SceneIo>().status =
            format!("Created {normalized}, but registry update failed: {error}");
        return;
    }
    world.resource_mut::<EditorAssets>().refresh();
    world
        .resource_mut::<EditorAssets>()
        .select(AssetSelection::File(normalized.clone()));
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = format!("Created material instance {normalized}");
}

fn material_base_candidates(world: &World) -> Vec<(String, MaterialRef)> {
    let registry = world.get_resource::<crate::asset_db::AssetRegistry>();
    let mut candidates = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Material))
        .filter_map(|asset| {
            let path = asset.path.as_deref()?;
            MaterialAsset::load_from_path(path).ok()?;
            let record = registry.and_then(|registry| find_asset_by_path(registry, path))?;
            Some((
                asset.label.clone(),
                MaterialRef::new(
                    EditorAssetRef::new(
                        record.id.as_string(),
                        "material:source",
                        asset.label.clone(),
                    )
                    .with_source_path(path),
                ),
            ))
        })
        .collect::<Vec<_>>();
    candidates.sort_by(|left, right| natural_cmp(&left.0, &right.0));
    candidates
}

fn material_instance_asset_editor(
    world: &mut World,
    ui: &mut egui::Ui,
    asset: &EditorAsset,
    path: &str,
) {
    ensure_material_instance_draft(world, path, &asset.label);
    let base_candidates = material_base_candidates(world);
    let texture_assets = material_texture_candidates(world);
    let texture_drop_candidate = world
        .resource::<EditorAssets>()
        .dragging_selection()
        .and_then(|selection| {
            texture_assets
                .iter()
                .find(|candidate| candidate.selection == *selection)
        })
        .cloned();
    let base_asset = world
        .resource::<AssetBrowserUiState>()
        .material_instance_draft
        .as_ref()
        .and_then(|draft| draft.asset.base.0.source_path.as_deref())
        .and_then(|path| MaterialAsset::load_from_path(path).ok());
    let schema = base_asset.as_ref().and_then(material_schema_for_asset);
    let mut apply = false;
    let mut revert = false;
    let mut accepted_texture_drop = false;
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        let Some(draft) = state.material_instance_draft.as_mut() else {
            return;
        };
        if let Some(error) = draft.error.as_ref() {
            ui.colored_label(egui::Color32::YELLOW, error);
        }
        compact_text_edit(ui, "Label", &mut draft.asset.label);
        ui.horizontal(|ui| {
            ui.add_sized([92.0, 20.0], egui::Label::new("Base material"));
            let selected = base_candidates
                .iter()
                .find(|(_, candidate)| candidate.0.asset_id == draft.asset.base.0.asset_id)
                .map(|(label, _)| label.as_str())
                .unwrap_or("Missing base");
            egui::ComboBox::from_id_salt("material_instance_base")
                .selected_text(selected)
                .show_ui(ui, |ui| {
                    for (label, reference) in &base_candidates {
                        if ui
                            .selectable_label(
                                reference.0.asset_id == draft.asset.base.0.asset_id,
                                label,
                            )
                            .clicked()
                        {
                            draft.asset.base = reference.clone();
                        }
                    }
                });
        });
        ui.small("Overrides are sparse; unchecked properties continue to inherit the base asset.");
        ui.separator();
        ui.label(panel_heading("Standard Overrides"));
        let base_desc = base_asset
            .as_ref()
            .map(|asset| &asset.material)
            .cloned()
            .unwrap_or_default();
        material_instance_parameter_override_ui(
            ui,
            "Base color",
            "base_color",
            ShaderPropertyType::Color,
            MaterialParameterValue::Color(base_desc.base_color),
            &mut draft.asset.parameters,
        );
        material_instance_parameter_override_ui(
            ui,
            "Metallic",
            "metallic",
            ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            MaterialParameterValue::Float(base_desc.metallic),
            &mut draft.asset.parameters,
        );
        material_instance_parameter_override_ui(
            ui,
            "Roughness",
            "roughness",
            ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            MaterialParameterValue::Float(base_desc.roughness),
            &mut draft.asset.parameters,
        );
        material_instance_parameter_override_ui(
            ui,
            "Emissive",
            "emissive_color",
            ShaderPropertyType::Color,
            MaterialParameterValue::Color(base_desc.emissive_color),
            &mut draft.asset.parameters,
        );
        material_instance_parameter_override_ui(
            ui,
            "Emissive nits",
            "emissive_intensity",
            ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(50_000.0),
            },
            MaterialParameterValue::Float(base_desc.emissive_intensity),
            &mut draft.asset.parameters,
        );
        if let Some(schema) = schema.as_ref() {
            let custom = schema
                .parameters
                .iter()
                .filter(|property| {
                    !matches!(property.property_type, ShaderPropertyType::Texture)
                        && !matches!(
                            property.name.as_str(),
                            "base_color"
                                | "metallic"
                                | "roughness"
                                | "emissive_color"
                                | "emissive_intensity"
                        )
                })
                .collect::<Vec<_>>();
            if !custom.is_empty() {
                ui.separator();
                ui.label(panel_heading("Shader Overrides"));
                for property in custom {
                    let inherited = base_desc
                        .parameters
                        .iter()
                        .find(|value| value.name == property.name)
                        .or_else(|| {
                            schema
                                .default_values
                                .iter()
                                .find(|value| value.name == property.name)
                        })
                        .map(|value| value.value.clone())
                        .unwrap_or_else(|| {
                            default_value_for_shader_property(&property.property_type)
                        });
                    material_instance_parameter_override_ui(
                        ui,
                        &property.display_name,
                        &property.name,
                        property.property_type.clone(),
                        inherited,
                        &mut draft.asset.parameters,
                    );
                }
            }
        }
        ui.separator();
        ui.label(panel_heading("Texture Overrides"));
        let mut texture_properties = vec![
            ("Base color", "base_color_texture"),
            ("Normal map", "normal_map_texture"),
            ("Metallic / roughness", "metallic_roughness_texture"),
            ("Emissive", "emissive_texture"),
        ];
        if let Some(schema) = schema.as_ref() {
            for property in schema
                .parameters
                .iter()
                .filter(|property| matches!(property.property_type, ShaderPropertyType::Texture))
            {
                if !texture_properties
                    .iter()
                    .any(|(_, name)| *name == property.name)
                {
                    texture_properties.push((&property.display_name, &property.name));
                }
            }
        }
        for (label, name) in texture_properties {
            accepted_texture_drop |= material_instance_texture_override_ui(
                ui,
                label,
                name,
                &mut draft.asset.textures,
                &texture_assets,
                texture_drop_candidate.as_ref(),
            );
        }
        ui.horizontal(|ui| {
            if ui.button("Apply").clicked() {
                apply = true;
            }
            if ui.button("Revert").clicked() {
                revert = true;
            }
        });
    }
    if revert {
        world
            .resource_mut::<AssetBrowserUiState>()
            .material_instance_draft = None;
        ensure_material_instance_draft(world, path, &asset.label);
    }
    if apply {
        save_material_instance_draft(world);
    }
    if accepted_texture_drop {
        world.resource_mut::<EditorAssets>().clear_drag();
    }
}

fn ensure_material_instance_draft(world: &mut World, path: &str, fallback_label: &str) {
    let needs_load = world
        .resource::<AssetBrowserUiState>()
        .material_instance_draft
        .as_ref()
        .is_none_or(|draft| draft.path != path);
    if !needs_load {
        return;
    }
    let loaded = fs::read_to_string(path)
        .map_err(|error| format!("could not read {path}: {error}"))
        .and_then(|text| {
            let snapshot = FileSnapshot::from_loaded_bytes(Path::new(path), text.as_bytes());
            ron::from_str::<MaterialInstanceAsset>(&text)
                .map(|asset| (asset, snapshot))
                .map_err(|error| format!("invalid material-instance RON in {path}: {error}"))
        });
    let draft = match loaded {
        Ok((asset, disk_snapshot)) => MaterialInstanceAssetDraft {
            path: path.to_string(),
            asset,
            disk_snapshot,
            error: None,
        },
        Err(error) => MaterialInstanceAssetDraft {
            path: path.to_string(),
            asset: MaterialInstanceAsset {
                schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
                label: fallback_label.to_string(),
                base: MaterialRef::default(),
                parameters: Vec::new(),
                textures: Vec::new(),
            },
            disk_snapshot: FileSnapshot::capture(Path::new(path))
                .unwrap_or_else(|_| FileSnapshot::missing()),
            error: Some(error),
        },
    };
    world
        .resource_mut::<AssetBrowserUiState>()
        .material_instance_draft = Some(draft);
}

fn save_material_instance_draft(world: &mut World) {
    let Some(draft) = world
        .resource::<AssetBrowserUiState>()
        .material_instance_draft
        .clone()
    else {
        return;
    };
    let result = if !draft.asset.base.is_resolved()
        || draft.asset.base.0.sub_asset_id != "material:source"
    {
        Err("Material Instance base must resolve directly to a Material asset".to_string())
    } else {
        ron::ser::to_string_pretty(&draft.asset, ron::ser::PrettyConfig::default())
            .map_err(|error| format!("could not serialize material instance: {error}"))
            .and_then(|text| {
                publish_authored_file(
                    world,
                    Path::new(&draft.path),
                    text.as_bytes(),
                    &draft.disk_snapshot,
                    FileWriteIntent::MaterialInstance,
                )
            })
    };
    match result {
        Ok(disk_snapshot) => {
            if let Some(current) = world
                .resource_mut::<AssetBrowserUiState>()
                .material_instance_draft
                .as_mut()
            {
                current.error = None;
                current.disk_snapshot = disk_snapshot;
            }
            world.resource_mut::<EditorAssets>().refresh();
            invalidate_on_catalog_refresh(world);
            world.resource_mut::<SceneIo>().status =
                format!("Saved material instance {}", draft.path);
        }
        Err(error) => {
            if let Some(current) = world
                .resource_mut::<AssetBrowserUiState>()
                .material_instance_draft
                .as_mut()
            {
                current.error = Some(error.clone());
            }
            world.resource_mut::<SceneIo>().status = error;
        }
    }
}

fn material_schema_for_asset(asset: &MaterialAsset) -> Option<ShaderSchemaAsset> {
    asset
        .shader_ref
        .as_ref()
        .and_then(|reference| reference.source_path.as_deref())
        .or(asset.material.shader.schema_path.as_deref())
        .or(asset
            .shader
            .as_deref()
            .filter(|path| path.ends_with(".ron")))
        .and_then(|path| ShaderSchemaAsset::load_from_path(path).ok())
}

fn material_instance_parameter_override_ui(
    ui: &mut egui::Ui,
    label: &str,
    name: &str,
    property_type: ShaderPropertyType,
    inherited: MaterialParameterValue,
    parameters: &mut Vec<MaterialParameter>,
) {
    let index = parameters.iter().position(|value| value.name == name);
    let mut enabled = index.is_some();
    if ui
        .checkbox(&mut enabled, format!("Override {label}"))
        .changed()
    {
        if enabled {
            parameters.push(MaterialParameter {
                name: name.to_string(),
                value: inherited,
            });
        } else {
            parameters.retain(|value| value.name != name);
        }
    }
    if let Some(value) = parameters.iter_mut().find(|value| value.name == name) {
        material_parameter_value_ui(ui, label, &property_type, &mut value.value);
    }
}

fn material_instance_texture_override_ui(
    ui: &mut egui::Ui,
    label: &str,
    name: &str,
    textures: &mut Vec<MaterialTextureBinding>,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> bool {
    let mut enabled = textures.iter().any(|value| value.name == name);
    if ui
        .checkbox(&mut enabled, format!("Override {label}"))
        .changed()
    {
        if enabled {
            textures.push(MaterialTextureBinding {
                name: name.to_string(),
                texture: None,
            });
        } else {
            textures.retain(|value| value.name != name);
        }
    }

    let current = textures
        .iter()
        .find(|value| value.name == name)
        .and_then(|binding| binding.texture.as_ref())
        .cloned();
    let response = texture_slot_ui(
        ui,
        label,
        format!("instance_{name}"),
        TextureSlotSelection::Reference(current.as_ref()),
        if enabled { "(none)" } else { "(inherits base)" },
        texture_assets,
        drop_candidate,
    );
    let accepted_drop = response.accepted_drop;
    if response.clear {
        textures.retain(|value| value.name != name);
    } else if let Some(candidate) = response.selected {
        set_material_instance_texture_override(textures, name, Some(candidate.reference));
    }
    accepted_drop
}

fn set_material_instance_texture_override(
    textures: &mut Vec<MaterialTextureBinding>,
    name: &str,
    texture: Option<EditorAssetRef>,
) {
    match texture {
        Some(texture) => {
            if let Some(binding) = textures.iter_mut().find(|value| value.name == name) {
                binding.texture = Some(texture);
            } else {
                textures.push(MaterialTextureBinding {
                    name: name.to_string(),
                    texture: Some(texture),
                });
            }
        }
        None => textures.retain(|value| value.name != name),
    }
}

fn shader_schema_assets(world: &World) -> Vec<(String, String, EditorAssetRef)> {
    let registry = world.get_resource::<crate::asset_db::AssetRegistry>();
    let mut schemas: Vec<(String, String, EditorAssetRef)> = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::ShaderSchema))
        .filter_map(|asset| {
            let path = asset.path.clone()?;
            let record = registry.and_then(|registry| find_asset_by_path(registry, &path))?;
            Some((
                asset.label.clone(),
                path.clone(),
                EditorAssetRef::new(record.id.as_string(), "shader:schema", asset.label.clone())
                    .with_source_path(path),
            ))
        })
        .collect();
    schemas.sort_by(|a, b| natural_cmp(&a.0, &b.0));
    schemas
}

fn material_texture_candidates(world: &World) -> Vec<MaterialTextureCandidate> {
    let Some(registry) = world.get_resource::<crate::asset_db::AssetRegistry>() else {
        return Vec::new();
    };
    let assets = &world.resource::<EditorAssets>().assets;
    let mut textures = Vec::new();
    let mut seen = HashSet::new();

    for asset in assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
    {
        let Some(path) = asset.path.as_deref() else {
            continue;
        };
        let Some(record) = find_asset_by_path(registry, path) else {
            continue;
        };
        let reference =
            EditorAssetRef::new(record.id.as_string(), "texture:source", asset.label.clone())
                .with_source_path(path);
        if !seen.insert((reference.asset_id.clone(), reference.sub_asset_id.clone())) {
            continue;
        }
        textures.push(MaterialTextureCandidate {
            label: asset.label.clone(),
            detail: path.to_string(),
            path: path.to_string(),
            reference,
            selection: AssetSelection::File(path.to_string()),
        });
    }

    for asset in assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Model))
    {
        let Some(parent_path) = asset.path.as_deref() else {
            continue;
        };
        let Some(record) = find_asset_by_path(registry, parent_path) else {
            continue;
        };
        for embedded in embedded_assets_for_asset(world, asset)
            .into_iter()
            .filter(|embedded| embedded.kind == AssetSubAssetKind::Texture)
        {
            let AssetSelection::SubAsset {
                sub_asset_id,
                source_path: Some(source_path),
                ..
            } = &embedded.selection
            else {
                continue;
            };
            let reference = EditorAssetRef::new(
                record.id.as_string(),
                sub_asset_id.clone(),
                embedded.label.clone(),
            )
            .with_source_path(source_path);
            if !seen.insert((reference.asset_id.clone(), reference.sub_asset_id.clone())) {
                continue;
            }
            textures.push(MaterialTextureCandidate {
                label: embedded.label,
                detail: format!("{} | {source_path}", asset.label),
                path: source_path.clone(),
                reference,
                selection: embedded.selection,
            });
        }
    }

    textures.sort_by(|left, right| {
        natural_cmp(&left.label, &right.label)
            .then_with(|| natural_cmp(&left.detail, &right.detail))
    });
    textures
}

fn material_shader_interface(
    ui: &mut egui::Ui,
    asset: &mut MaterialAsset,
    shader_schemas: &[(String, String, EditorAssetRef)],
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    accepted_texture_drop: &mut bool,
) {
    shader_kind_picker(ui, &mut asset.material.shader.kind);
    if matches!(asset.material.shader.kind, MaterialShaderKind::Custom) {
        let mut selected_schema = asset.material.shader.schema_path.clone();
        ui.horizontal(|ui| {
            ui.add_sized([92.0, 20.0], egui::Label::new("Schema"));
            egui::ComboBox::from_id_salt("asset_material_shader_schema")
                .selected_text(
                    selected_schema
                        .as_deref()
                        .and_then(|path| {
                            shader_schemas
                                .iter()
                                .find(|(_, schema_path, _)| schema_path == path)
                                .map(|(label, _, _)| label.as_str())
                        })
                        .unwrap_or("(none)"),
                )
                .show_ui(ui, |ui| {
                    if ui
                        .selectable_label(selected_schema.is_none(), "(none)")
                        .clicked()
                    {
                        selected_schema = None;
                    }
                    for (label, path, _) in shader_schemas {
                        if ui
                            .selectable_label(selected_schema.as_deref() == Some(path), label)
                            .clicked()
                        {
                            selected_schema = Some(path.clone());
                        }
                    }
                });
        });
        if selected_schema != asset.material.shader.schema_path {
            asset.material.shader.schema_path = selected_schema.clone();
            if let Some(path) = selected_schema.as_deref() {
                asset.shader_ref = shader_schemas
                    .iter()
                    .find(|(_, schema_path, _)| schema_path == path)
                    .map(|(_, _, reference)| reference.clone());
                if let Err(error) = apply_shader_schema(asset, path) {
                    ui.colored_label(egui::Color32::YELLOW, error);
                }
            } else {
                asset.shader_ref = None;
            }
        }
        optional_path_edit(ui, "WGSL shader", &mut asset.material.shader.shader_path);
        if let Some(path) = asset.material.shader.schema_path.clone() {
            ui.horizontal(|ui| {
                if ui.button("Reload Schema").clicked() {
                    if let Err(error) = apply_shader_schema(asset, &path) {
                        ui.colored_label(egui::Color32::YELLOW, error);
                    }
                }
                ui.small(egui::RichText::new(path).color(TEXT_DIM));
            });
        }
    } else {
        asset.shader_ref = None;
        asset.material.shader.schema_path = None;
        asset.material.shader.shader_path = None;
    }

    let schema = asset
        .material
        .shader
        .schema_path
        .as_deref()
        .and_then(|path| ShaderSchemaAsset::load_from_path(path).ok());
    if let Some(schema) = schema.as_ref() {
        ui.separator();
        ui.label(panel_heading("Shader Parameters"));
        shader_schema_parameter_editor(ui, &schema.parameters, &mut asset.material.parameters);
        shader_schema_texture_editor(
            ui,
            &schema.parameters,
            &mut asset.material.textures,
            texture_assets,
            drop_candidate,
            accepted_texture_drop,
        );
    } else if !asset.material.parameters.is_empty() || !asset.material.textures.is_empty() {
        ui.separator();
        ui.label(panel_heading("Stored Shader Values"));
        raw_material_parameter_editor(ui, &mut asset.material.parameters);
        raw_material_texture_editor(
            ui,
            &mut asset.material.textures,
            texture_assets,
            drop_candidate,
            accepted_texture_drop,
        );
    }
}

fn apply_shader_schema(asset: &mut MaterialAsset, path: &str) -> Result<(), String> {
    let schema = ShaderSchemaAsset::load_from_path(path)?;
    asset.shader = Some(schema.label.clone());
    asset.material.shader.kind = schema.kind;
    asset.material.shader.schema_path = Some(path.to_string());
    if asset.material.shader.shader_path.is_none() {
        asset.material.shader.shader_path = schema.wgsl_path.clone();
    }

    for property in &schema.parameters {
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            if !asset
                .material
                .textures
                .iter()
                .any(|texture| texture.name == property.name)
            {
                let default = schema
                    .default_textures
                    .iter()
                    .find(|texture| texture.name == property.name)
                    .cloned()
                    .unwrap_or(MaterialTextureBinding {
                        name: property.name.clone(),
                        texture: None,
                    });
                asset.material.textures.push(default);
            }
            continue;
        }

        let default = schema
            .default_values
            .iter()
            .find(|parameter| parameter.name == property.name)
            .map(|parameter| parameter.value.clone())
            .unwrap_or_else(|| default_value_for_shader_property(&property.property_type));
        match asset
            .material
            .parameters
            .iter_mut()
            .find(|parameter| parameter.name == property.name)
        {
            Some(parameter) => {
                if !parameter_value_matches_property(&parameter.value, &property.property_type) {
                    parameter.value = default;
                }
            }
            None => asset.material.parameters.push(MaterialParameter {
                name: property.name.clone(),
                value: default,
            }),
        }
    }

    Ok(())
}

fn shader_schema_parameter_editor(
    ui: &mut egui::Ui,
    properties: &[ShaderPropertyDesc],
    parameters: &mut Vec<MaterialParameter>,
) {
    for property in properties
        .iter()
        .filter(|property| !matches!(property.property_type, ShaderPropertyType::Texture))
    {
        let value = ensure_material_parameter(parameters, property);
        material_parameter_value_ui(ui, &property.display_name, &property.property_type, value);
    }
}

fn shader_schema_texture_editor(
    ui: &mut egui::Ui,
    properties: &[ShaderPropertyDesc],
    textures: &mut Vec<MaterialTextureBinding>,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    accepted_texture_drop: &mut bool,
) {
    let texture_props: Vec<&ShaderPropertyDesc> = properties
        .iter()
        .filter(|property| matches!(property.property_type, ShaderPropertyType::Texture))
        .collect();
    if texture_props.is_empty() {
        return;
    }
    ui.separator();
    ui.label(panel_heading("Shader Textures"));
    for property in texture_props {
        let texture = ensure_material_texture_binding(textures, &property.name);
        *accepted_texture_drop |= texture_binding_ui(
            ui,
            &property.display_name,
            texture,
            texture_assets,
            drop_candidate,
        );
    }
}

fn raw_material_parameter_editor(ui: &mut egui::Ui, parameters: &mut [MaterialParameter]) {
    for parameter in parameters {
        let property_type = property_type_for_value(&parameter.value);
        material_parameter_value_ui(ui, &parameter.name, &property_type, &mut parameter.value);
    }
}

fn raw_material_texture_editor(
    ui: &mut egui::Ui,
    textures: &mut [MaterialTextureBinding],
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    accepted_texture_drop: &mut bool,
) {
    if textures.is_empty() {
        return;
    }
    ui.separator();
    ui.label(panel_heading("Stored Shader Textures"));
    for texture in textures {
        let label = texture.name.clone();
        *accepted_texture_drop |=
            texture_binding_ui(ui, &label, texture, texture_assets, drop_candidate);
    }
}

fn ensure_material_parameter<'a>(
    parameters: &'a mut Vec<MaterialParameter>,
    property: &ShaderPropertyDesc,
) -> &'a mut MaterialParameterValue {
    let index = parameters
        .iter()
        .position(|parameter| parameter.name == property.name)
        .unwrap_or_else(|| {
            parameters.push(MaterialParameter {
                name: property.name.clone(),
                value: default_value_for_shader_property(&property.property_type),
            });
            parameters.len() - 1
        });
    if !parameter_value_matches_property(&parameters[index].value, &property.property_type) {
        parameters[index].value = default_value_for_shader_property(&property.property_type);
    }
    &mut parameters[index].value
}

fn ensure_material_texture_binding<'a>(
    textures: &'a mut Vec<MaterialTextureBinding>,
    name: &str,
) -> &'a mut MaterialTextureBinding {
    let index = textures
        .iter()
        .position(|texture| texture.name == name)
        .unwrap_or_else(|| {
            textures.push(MaterialTextureBinding {
                name: name.to_string(),
                texture: None,
            });
            textures.len() - 1
        });
    &mut textures[index]
}

fn material_parameter_value_ui(
    ui: &mut egui::Ui,
    label: &str,
    property_type: &ShaderPropertyType,
    value: &mut MaterialParameterValue,
) {
    if !parameter_value_matches_property(value, property_type) {
        *value = default_value_for_shader_property(property_type);
    }
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        match value {
            MaterialParameterValue::Bool(value) => {
                ui.checkbox(value, "");
            }
            MaterialParameterValue::Float(value) => {
                let (min, max) = match property_type {
                    ShaderPropertyType::Float { min, max } => (*min, *max),
                    _ => (None, None),
                };
                match (min, max) {
                    (Some(min), Some(max)) => {
                        ui.add(egui::Slider::new(value, min..=max));
                    }
                    _ => {
                        ui.add(egui::DragValue::new(value).speed(0.01));
                    }
                }
            }
            MaterialParameterValue::Vec2(value) => {
                ui.add_sized([64.0, 20.0], egui::DragValue::new(&mut value.x).speed(0.01));
                ui.add_sized([64.0, 20.0], egui::DragValue::new(&mut value.y).speed(0.01));
            }
            MaterialParameterValue::Vec3(value) => {
                ui.add_sized([52.0, 20.0], egui::DragValue::new(&mut value.x).speed(0.01));
                ui.add_sized([52.0, 20.0], egui::DragValue::new(&mut value.y).speed(0.01));
                ui.add_sized([52.0, 20.0], egui::DragValue::new(&mut value.z).speed(0.01));
            }
            MaterialParameterValue::Color(value) => {
                let mut rgba = [value.r, value.g, value.b, value.a];
                if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
                    *value = ColorDesc {
                        r: rgba[0],
                        g: rgba[1],
                        b: rgba[2],
                        a: rgba[3],
                    };
                }
            }
            MaterialParameterValue::Enum(value) => {
                let options = match property_type {
                    ShaderPropertyType::Enum { options } => options.as_slice(),
                    _ => &[],
                };
                egui::ComboBox::from_id_salt(format!("material_enum_{label}"))
                    .selected_text(value.as_str())
                    .show_ui(ui, |ui| {
                        for option in options {
                            ui.selectable_value(value, option.clone(), option);
                        }
                    });
            }
        }
    });
}

fn texture_binding_ui(
    ui: &mut egui::Ui,
    label: &str,
    binding: &mut MaterialTextureBinding,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> bool {
    let response = texture_slot_ui(
        ui,
        label,
        format!("binding_{}", binding.name),
        TextureSlotSelection::Reference(binding.texture.as_ref()),
        "(none)",
        texture_assets,
        drop_candidate,
    );
    let accepted_drop = response.accepted_drop;
    if response.clear {
        binding.texture = None;
    } else if let Some(candidate) = response.selected {
        binding.texture = Some(candidate.reference);
    }
    accepted_drop
}

fn material_texture_path_ui(
    ui: &mut egui::Ui,
    label: &str,
    slot_id: &str,
    value: &mut Option<String>,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> bool {
    let response = texture_slot_ui(
        ui,
        label,
        slot_id,
        TextureSlotSelection::Path(value.as_deref()),
        "(none)",
        texture_assets,
        drop_candidate,
    );
    let accepted_drop = response.accepted_drop;
    if response.clear {
        *value = None;
    } else if let Some(candidate) = response.selected {
        *value = Some(candidate.path);
    }
    accepted_drop
}

fn texture_slot_ui(
    ui: &mut egui::Ui,
    label: &str,
    slot_id: impl std::hash::Hash,
    current: TextureSlotSelection<'_>,
    empty_label: &str,
    candidates: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> TextureSlotResponse {
    let mut result = TextureSlotResponse::default();
    ui.push_id(slot_id, |ui| {
        ui.horizontal(|ui| {
            ui.add_sized([92.0, 20.0], egui::Label::new(label));
            let width = ui.available_width().max(1.0);
            let (rect, hover) =
                ui.allocate_exact_size(egui::vec2(width, 30.0), egui::Sense::hover());
            let valid_drag = drop_candidate.is_some();
            let drop_hovered = valid_drag && ui.rect_contains_pointer(rect);
            let row_hovered = hover.hovered();
            let stroke = if drop_hovered {
                egui::Stroke::new(2.0, egui::Color32::from_rgb(125, 198, 255))
            } else if valid_drag {
                egui::Stroke::new(1.0, egui::Color32::from_rgb(58, 88, 122))
            } else if row_hovered {
                egui::Stroke::new(1.0, egui::Color32::from_rgb(92, 102, 118))
            } else {
                egui::Stroke::new(1.0, BORDER)
            };
            let fill = if drop_hovered {
                egui::Color32::from_rgb(29, 57, 86)
            } else if row_hovered {
                WIDGET_BG.linear_multiply(1.05)
            } else {
                WIDGET_BG.linear_multiply(0.75)
            };
            ui.painter()
                .rect(rect, 4.0, fill, stroke, egui::StrokeKind::Inside);

            let selected_candidate = candidates.iter().find(|candidate| match current {
                TextureSlotSelection::Path(path) => path == Some(candidate.path.as_str()),
                TextureSlotSelection::Reference(reference) => reference.is_some_and(|reference| {
                    reference.asset_id == candidate.reference.asset_id
                        && reference.sub_asset_id == candidate.reference.sub_asset_id
                }),
            });
            let current_present = match current {
                TextureSlotSelection::Path(path) => path.is_some(),
                TextureSlotSelection::Reference(reference) => reference.is_some(),
            };
            let display_label = selected_candidate
                .map(|candidate| candidate.label.as_str())
                .or_else(|| match current {
                    TextureSlotSelection::Path(path) => path,
                    TextureSlotSelection::Reference(reference) => {
                        reference.map(|reference| reference.label.as_str())
                    }
                })
                .unwrap_or(empty_label);

            let mut child = ui.new_child(
                egui::UiBuilder::new()
                    .max_rect(rect.shrink2(egui::vec2(5.0, 3.0)))
                    .layout(egui::Layout::left_to_right(egui::Align::Center)),
            );
            child.set_clip_rect(rect);
            child.add_sized(
                [18.0, 20.0],
                egui::Label::new(icon_text(icons::IMAGE, 13.0).color(TEXT_DIM)),
            );
            let action_width = 56.0;
            let text_width =
                (child.available_width() - action_width - child.spacing().item_spacing.x).max(1.0);
            child.allocate_ui_with_layout(
                egui::vec2(text_width, 22.0),
                egui::Layout::left_to_right(egui::Align::Center),
                |ui| {
                    ui.add_sized(
                        [text_width, 20.0],
                        egui::Label::new(display_label).truncate(),
                    );
                },
            );
            child.allocate_ui_with_layout(
                egui::vec2(action_width, 24.0),
                egui::Layout::right_to_left(egui::Align::Center),
                |ui| {
                    let clear = ui.add_enabled(
                        current_present,
                        egui::Button::new(icon_text(icons::X, 14.0))
                            .frame(false)
                            .min_size(egui::vec2(20.0, 20.0)),
                    );
                    if clear.on_hover_text("Clear texture").clicked() {
                        result.clear = true;
                    }
                    if candidates.is_empty() {
                        ui.add_enabled(
                            false,
                            egui::Button::new(icon_text(icons::FOLDER_OPEN, 14.0))
                                .frame(false)
                                .min_size(egui::vec2(20.0, 20.0)),
                        )
                        .on_hover_text("No project textures available");
                    } else {
                        let menu = ui.menu_button(icon_text(icons::FOLDER_OPEN, 14.0), |ui| {
                            ui.set_min_width(240.0);
                            for candidate in candidates {
                                let selected = selected_candidate.is_some_and(|selected| {
                                    selected.reference.asset_id == candidate.reference.asset_id
                                        && selected.reference.sub_asset_id
                                            == candidate.reference.sub_asset_id
                                });
                                if ui
                                    .selectable_label(selected, candidate.label.as_str())
                                    .on_hover_text(candidate.detail.as_str())
                                    .clicked()
                                {
                                    result.selected = Some(candidate.clone());
                                    ui.close();
                                }
                            }
                        });
                        menu.response.on_hover_text("Select a project texture");
                    }
                },
            );

            if drop_hovered && ui.input(|input| input.pointer.any_released()) {
                if let Some(candidate) = drop_candidate {
                    result.selected = Some(candidate.clone());
                    result.accepted_drop = true;
                }
            }
        });
    });
    result
}

fn default_value_for_shader_property(property_type: &ShaderPropertyType) -> MaterialParameterValue {
    match property_type {
        ShaderPropertyType::Bool => MaterialParameterValue::Bool(false),
        ShaderPropertyType::Float { .. } => MaterialParameterValue::Float(0.0),
        ShaderPropertyType::Vec2 => MaterialParameterValue::Vec2(Vec2::ZERO),
        ShaderPropertyType::Vec3 => MaterialParameterValue::Vec3(Vec3::ZERO),
        ShaderPropertyType::Color => MaterialParameterValue::Color(ColorDesc::default()),
        ShaderPropertyType::Enum { options } => {
            MaterialParameterValue::Enum(options.first().cloned().unwrap_or_default())
        }
        ShaderPropertyType::Texture => MaterialParameterValue::Float(0.0),
    }
}

fn parameter_value_matches_property(
    value: &MaterialParameterValue,
    property_type: &ShaderPropertyType,
) -> bool {
    matches!(
        (value, property_type),
        (MaterialParameterValue::Bool(_), ShaderPropertyType::Bool)
            | (
                MaterialParameterValue::Float(_),
                ShaderPropertyType::Float { .. }
            )
            | (MaterialParameterValue::Vec2(_), ShaderPropertyType::Vec2)
            | (MaterialParameterValue::Vec3(_), ShaderPropertyType::Vec3)
            | (MaterialParameterValue::Color(_), ShaderPropertyType::Color)
            | (
                MaterialParameterValue::Enum(_),
                ShaderPropertyType::Enum { .. }
            )
    )
}

fn property_type_for_value(value: &MaterialParameterValue) -> ShaderPropertyType {
    match value {
        MaterialParameterValue::Bool(_) => ShaderPropertyType::Bool,
        MaterialParameterValue::Float(_) => ShaderPropertyType::Float {
            min: None,
            max: None,
        },
        MaterialParameterValue::Vec2(_) => ShaderPropertyType::Vec2,
        MaterialParameterValue::Vec3(_) => ShaderPropertyType::Vec3,
        MaterialParameterValue::Color(_) => ShaderPropertyType::Color,
        MaterialParameterValue::Enum(_) => ShaderPropertyType::Enum {
            options: Vec::new(),
        },
    }
}

fn compact_text_edit(ui: &mut egui::Ui, label: &str, value: &mut String) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        ui.add_sized(
            [fit_width(ui, 100.0, f32::INFINITY), 22.0],
            egui::TextEdit::singleline(value),
        );
    });
}

fn shader_kind_picker(ui: &mut egui::Ui, kind: &mut MaterialShaderKind) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new("Shader"));
        egui::ComboBox::from_id_salt("asset_material_shader_kind")
            .selected_text(match kind {
                MaterialShaderKind::StandardLit => "Standard Lit",
                MaterialShaderKind::Unlit => "Unlit",
                MaterialShaderKind::Custom => "Custom",
            })
            .show_ui(ui, |ui| {
                ui.selectable_value(kind, MaterialShaderKind::StandardLit, "Standard Lit");
                ui.selectable_value(kind, MaterialShaderKind::Unlit, "Unlit");
                ui.selectable_value(kind, MaterialShaderKind::Custom, "Custom");
            });
    });
}

fn color_desc_edit(ui: &mut egui::Ui, label: &str, color: &mut ColorDesc) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        let mut rgba = [color.r, color.g, color.b, color.a];
        if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
            *color = ColorDesc {
                r: rgba[0],
                g: rgba[1],
                b: rgba[2],
                a: rgba[3],
            };
        }
    });
}

fn optional_path_edit(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        let mut text = value.clone().unwrap_or_default();
        if ui
            .add_sized(
                [fit_width(ui, 80.0, f32::INFINITY), 22.0],
                egui::TextEdit::singleline(&mut text),
            )
            .changed()
        {
            *value = if text.trim().is_empty() {
                None
            } else {
                Some(text)
            };
        }
        if ui.button("Clear").clicked() {
            *value = None;
        }
    });
}

fn texture_asset_from_subasset_selection(
    world: &World,
    selection: &Option<AssetSelection>,
) -> Option<EditorAsset> {
    let Some(AssetSelection::SubAsset {
        label,
        kind: AssetSubAssetKind::Texture,
        source_path: Some(path),
        ..
    }) = selection
    else {
        return None;
    };
    if let Some(asset) = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .find(|asset| asset.path.as_deref() == Some(path.as_str()))
    {
        return Some(asset.clone());
    }
    Some(EditorAsset {
        label: label.clone(),
        path: Some(path.clone()),
        folder_path: Path::new(path)
            .parent()
            .map(|parent| parent.to_string_lossy().replace('\\', "/"))
            .unwrap_or_else(|| ASSETS_ROOT.to_string()),
        kind: EditorAssetKind::Texture,
    })
}

fn detail_row(ui: &mut egui::Ui, label: &str, value: &str) {
    ui.horizontal_wrapped(|ui| {
        ui.label(egui::RichText::new(label).color(TEXT_DIM));
        ui.add(egui::Label::new(value).wrap().selectable(false));
    });
}

fn empty_content(ui: &mut egui::Ui, search: &str) {
    ui.add_space(24.0);
    ui.vertical_centered(|ui| {
        ui.label(
            egui::RichText::new(icons::FOLDER_OPEN.as_str()).font(egui::FontId::new(
                28.0,
                egui::FontFamily::Name("phosphor-regular".into()),
            )),
        );
        if search.trim().is_empty() {
            ui.label(egui::RichText::new("Folder is empty").color(TEXT_DIM));
        } else {
            ui.label(egui::RichText::new("No matching assets").color(TEXT_DIM));
        }
    });
}

fn navigate_to_parent(world: &mut World) {
    let current = world.resource::<EditorAssets>().current_folder.clone();
    if let Some(parent) = current.rfind('/').map(|index| current[..index].to_string()) {
        world.resource_mut::<EditorAssets>().current_folder = parent;
    }
}

fn asset_matches_kind_filter(asset: &EditorAsset, filter: AssetKindFilter) -> bool {
    match filter {
        AssetKindFilter::All => true,
        AssetKindFilter::Model => matches!(asset.kind, EditorAssetKind::Model),
        AssetKindFilter::Texture => matches!(asset.kind, EditorAssetKind::Texture),
        AssetKindFilter::Material => matches!(asset.kind, EditorAssetKind::Material),
        AssetKindFilter::Audio => matches!(asset.kind, EditorAssetKind::AudioClip),
        AssetKindFilter::Level => matches!(asset.kind, EditorAssetKind::Level),
        AssetKindFilter::Prefab => matches!(asset.kind, EditorAssetKind::Prefab),
        AssetKindFilter::Builtin => asset.path.is_none(),
    }
}

fn kind_filter_label(filter: AssetKindFilter) -> &'static str {
    match filter {
        AssetKindFilter::All => "All",
        AssetKindFilter::Model => "Models",
        AssetKindFilter::Texture => "Textures",
        AssetKindFilter::Material => "Materials",
        AssetKindFilter::Audio => "Audio",
        AssetKindFilter::Level => "Levels",
        AssetKindFilter::Prefab => "Prefabs",
        AssetKindFilter::Builtin => "Built-ins",
    }
}

fn sort_label(sort: AssetSort) -> &'static str {
    match sort {
        AssetSort::Name => "Name",
        AssetSort::Kind => "Type",
        AssetSort::Modified => "Modified",
        AssetSort::Size => "Size",
    }
}

fn kind_label(kind: &EditorAssetKind) -> &'static str {
    match kind {
        EditorAssetKind::Primitive(_) => "Primitive",
        EditorAssetKind::Light(_) => "Light",
        EditorAssetKind::Model => "Model",
        EditorAssetKind::Texture => "Texture",
        EditorAssetKind::Material => "Material",
        EditorAssetKind::AudioClip => "Audio Clip",
        EditorAssetKind::Level => "Level",
        EditorAssetKind::Prefab => "Prefab",
        EditorAssetKind::PostProcessVolume => "Post Process Volume",
        EditorAssetKind::PostProcessEffect => "Post FX",
        EditorAssetKind::RenderingProfile => "Rendering Profile",
        EditorAssetKind::ShaderSchema => "Shader Schema",
    }
}

fn file_size(path: &str) -> Option<u64> {
    fs::metadata(path).ok().map(|metadata| metadata.len())
}

fn audio_format_label(path: &str) -> &'static str {
    match Path::new(path)
        .extension()
        .and_then(|extension| extension.to_str())
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("ogg" | "oga") => "Ogg audio",
        Some("spx") => "Speex",
        Some("wav") => "WAV",
        Some("mp3") => "MP3",
        Some("flac") => "FLAC",
        _ => "Unknown",
    }
}

fn modified_time(path: &str) -> Option<SystemTime> {
    fs::metadata(path)
        .ok()
        .and_then(|metadata| metadata.modified().ok())
}

fn format_bytes(bytes: u64) -> String {
    const KIB: f64 = 1024.0;
    const MIB: f64 = KIB * 1024.0;
    const GIB: f64 = MIB * 1024.0;
    let bytes = bytes as f64;
    if bytes >= GIB {
        format!("{:.1} GB", bytes / GIB)
    } else if bytes >= MIB {
        format!("{:.1} MB", bytes / MIB)
    } else if bytes >= KIB {
        format!("{:.1} KB", bytes / KIB)
    } else {
        format!("{bytes:.0} B")
    }
}

fn format_modified(time: SystemTime) -> String {
    let Ok(duration) = SystemTime::now().duration_since(time) else {
        return "Just now".to_string();
    };
    let days = duration.as_secs() / 86_400;
    if days == 0 {
        "Today".to_string()
    } else if days == 1 {
        "Yesterday".to_string()
    } else if days < 30 {
        format!("{days} days ago")
    } else if days < 365 {
        format!("{} months ago", days / 30)
    } else {
        format!("{} years ago", days / 365)
    }
}

fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
    a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase())
}

fn asset_context_menu(
    world: &mut World,
    ui: &mut egui::Ui,
    asset: &EditorAsset,
    selected_entities: &SelectedEntities,
) {
    if ui.button("Select").clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(AssetSelection::from_asset(asset));
        ui.close();
    }
    if matches!(
        asset.kind,
        EditorAssetKind::Primitive(_)
            | EditorAssetKind::Light(_)
            | EditorAssetKind::Model
            | EditorAssetKind::AudioClip
            | EditorAssetKind::Prefab
    ) && ui.button("Place at Origin").clicked()
    {
        place_asset_operator(world, asset.clone(), Vec3::ZERO);
        ui.close();
    }
    if matches!(asset.kind, EditorAssetKind::Level) && ui.button("Open Scene").clicked() {
        open_level_asset(world, asset);
        ui.close();
    }
    if matches!(asset.kind, EditorAssetKind::Model) && has_embedded_assets(world, asset) {
        let expanded = asset.path.as_deref().is_some_and(|path| {
            world
                .resource::<AssetBrowserUiState>()
                .expanded_assets
                .contains(path)
        });
        if ui
            .button(if expanded {
                "Collapse Contents"
            } else {
                "Expand Contents"
            })
            .clicked()
        {
            if let Some(path) = asset.path.as_ref() {
                toggle_asset_expanded(world, path);
            }
            ui.close();
        }
    }
    if matches!(asset.kind, EditorAssetKind::Texture)
        && ui.button("Apply as Base Color Texture").clicked()
    {
        apply_texture_operator(world, asset.clone(), selected_entities);
        ui.close();
    }
    if matches!(asset.kind, EditorAssetKind::Material)
        && ui.button("Apply Material To Selection").clicked()
    {
        apply_material_operator(world, asset.clone(), selected_entities);
        ui.close();
    }
    if matches!(asset.kind, EditorAssetKind::Model) && ui.button("Reimport").clicked() {
        reimport_asset(world, asset);
        ui.close();
    }
    if matches!(
        asset.kind,
        EditorAssetKind::Texture | EditorAssetKind::Model | EditorAssetKind::Material
    ) && ui.button("Regenerate Thumbnail").clicked()
    {
        regenerate_asset_thumbnail(world, asset);
        ui.close();
    }
    if asset.path.is_some() {
        ui.separator();
        if ui.button("Move To Trash").clicked() {
            request_delete_for_asset(world, asset);
            ui.close();
        }
    }
}

fn subasset_context_menu(
    world: &mut World,
    ui: &mut egui::Ui,
    embedded: &EmbeddedAsset,
    selected_entities: &SelectedEntities,
) {
    if ui.button("Select").clicked() {
        world
            .resource_mut::<EditorAssets>()
            .select(embedded.selection.clone());
        ui.close();
    }
    match embedded.kind {
        AssetSubAssetKind::Mesh => {
            if embedded.requires_skinned_hierarchy {
                if ui.button("Place Skinned Model at Origin").clicked() {
                    place_subasset_operator(world, embedded.selection.clone(), Vec3::ZERO);
                    ui.close();
                }
            } else if ui.button("Place at Origin").clicked() {
                place_subasset_operator(world, embedded.selection.clone(), Vec3::ZERO);
                ui.close();
            }
        }
        AssetSubAssetKind::Texture => {
            if ui.button("Apply as Base Color Texture").clicked() {
                if let Some(asset) =
                    texture_asset_from_subasset_selection(world, &Some(embedded.selection.clone()))
                {
                    apply_texture_operator(world, asset, selected_entities);
                }
                ui.close();
            }
        }
        AssetSubAssetKind::Material => {
            ui.label(egui::RichText::new("Embedded source material").color(TEXT_DIM));
        }
        AssetSubAssetKind::Skeleton => {
            ui.label(egui::RichText::new("Inspect-only rig metadata").color(TEXT_DIM));
        }
        AssetSubAssetKind::AnimationClip => {
            let animated_actor = selected_entities
                .as_slice()
                .iter()
                .copied()
                .find(|entity| world.get::<shared::SkinnedMeshRenderer>(*entity).is_some());
            let action = if animated_actor.is_some() {
                "Assign To Selected Actor"
            } else {
                "Create Animated Actor"
            };
            if ui.button(action).clicked() {
                if let Some(entity) = animated_actor {
                    assign_animation_clip_operator(world, embedded.selection.clone(), entity);
                } else {
                    place_subasset_operator(world, embedded.selection.clone(), Vec3::ZERO);
                }
                ui.close();
            }
        }
    }
    if matches!(
        embedded.kind,
        AssetSubAssetKind::Mesh | AssetSubAssetKind::Material | AssetSubAssetKind::Texture
    ) && ui.button("Regenerate Thumbnail").clicked()
    {
        regenerate_subasset_thumbnail(world, embedded);
        ui.close();
    }
    if let AssetSelection::SubAsset { parent_path, .. } = &embedded.selection {
        if ui.button("Select Parent Asset").clicked() {
            world
                .resource_mut::<EditorAssets>()
                .select(AssetSelection::File(parent_path.clone()));
            ui.close();
        }
    }
}

fn regenerate_asset_thumbnail(world: &mut World, asset: &EditorAsset) {
    let Some(path) = asset.path.clone() else {
        return;
    };
    let key = asset_cache_key(asset);
    let kind = asset.kind.clone();
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        cache.retry(&key);
        match kind {
            EditorAssetKind::Texture => {
                cache.request_texture(key.clone(), path.clone(), &asset_server);
            }
            EditorAssetKind::Model => {
                world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
                    cache.request_model(key.clone(), path.clone(), &asset_server, &mut studio);
                });
            }
            EditorAssetKind::Material => {
                world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
                    cache.request_material_asset(key.clone(), path.clone(), &mut studio);
                });
            }
            _ => {}
        }
    });
    world.resource_mut::<SceneIo>().status = format!("Regenerating thumbnail for {}", asset.label);
}

fn regenerate_subasset_thumbnail(world: &mut World, embedded: &EmbeddedAsset) {
    let AssetSelection::SubAsset { parent_path, .. } = &embedded.selection else {
        return;
    };
    let key = embedded.thumbnail_key.clone();
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        cache.retry(&key);
        match embedded.kind {
            AssetSubAssetKind::Mesh => {
                let Some(mesh_label) = embedded.mesh_label.clone() else {
                    return;
                };
                world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
                    cache.request_mesh_subasset(
                        key.clone(),
                        parent_path.clone(),
                        mesh_label,
                        embedded.material_label.clone(),
                        embedded.requires_skinned_hierarchy,
                        &mut studio,
                    );
                });
            }
            AssetSubAssetKind::Material => {
                let Some(material_label) = embedded.material_label.clone() else {
                    return;
                };
                world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
                    cache.request_source_material(
                        key.clone(),
                        parent_path.clone(),
                        material_label,
                        &mut studio,
                    );
                });
            }
            AssetSubAssetKind::Texture => {
                if let Some(texture_path) = embedded.texture_path.clone() {
                    cache.request_texture(key.clone(), texture_path, &asset_server);
                }
            }
            AssetSubAssetKind::Skeleton | AssetSubAssetKind::AnimationClip => {}
        }
    });
    world.resource_mut::<SceneIo>().status =
        format!("Regenerating thumbnail for {}", embedded.label);
}

fn reimport_asset(world: &mut World, asset: &EditorAsset) {
    let Some(path) = asset.path.as_deref() else {
        return;
    };
    let mut status = format!("Reimported {}", asset.label);
    if let Some(mut registry) = world.get_resource_mut::<crate::asset_db::AssetRegistry>() {
        if let Some(record) = find_asset_mut_by_path(&mut registry, path) {
            if let Err(error) = refresh_model_artifacts(record) {
                status = format!("Reimport failed for {}: {error}", asset.label);
                warn!("{status}");
            }
        }
        if let Err(error) = save_registry(&registry) {
            status = format!("Asset registry save failed: {error}");
            warn!("{status}");
        } else {
            registry.index_dirty = false;
        }
    }
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = status;
}

fn request_delete_for_asset(world: &mut World, asset: &EditorAsset) {
    if let Some(request) = build_delete_request(world, asset) {
        world.resource_mut::<AssetBrowserUiState>().pending_delete = Some(request);
    }
}

fn build_delete_request(world: &World, asset: &EditorAsset) -> Option<AssetDeleteRequest> {
    let path = asset.path.clone()?;
    let mut files = vec![path.clone()];
    let mut warnings = Vec::new();
    if matches!(asset.kind, EditorAssetKind::Model) {
        if let Some(record) = world
            .get_resource::<crate::asset_db::AssetRegistry>()
            .and_then(|registry| find_asset_by_path(registry, &path))
        {
            if let Some(manifest) = record.import_settings.static_mesh_manifest_path {
                files.push(manifest);
            }
            if let Some(manifest) = record.import_settings.animation_manifest_path {
                files.push(manifest);
            }
            if !record.dependencies.is_empty() {
                warnings.push(format!(
                    "{} imported dependencies will be kept because they may be shared.",
                    record.dependencies.len()
                ));
            }
        }
    }
    warnings.push("Scene and prefab references are not rewritten automatically.".to_string());
    dedup_strings(&mut files);
    Some(AssetDeleteRequest {
        selection: AssetSelection::from_asset(asset),
        label: asset.label.clone(),
        files,
        warnings,
    })
}

fn draw_delete_modal(world: &mut World, ctx: &egui::Context) {
    let pending = world
        .resource::<AssetBrowserUiState>()
        .pending_delete
        .clone();
    let Some(request) = pending else {
        return;
    };
    let mut action = None;
    egui::Window::new("Move Asset To Trash")
        .collapsible(false)
        .resizable(false)
        .default_width(360.0)
        .show(ctx, |ui| {
            ui.label(format!("Move {} to assets/.trash?", request.label));
            ui.separator();
            ui.label(panel_heading("Files"));
            for file in &request.files {
                ui.add(egui::Label::new(file).wrap());
            }
            if !request.warnings.is_empty() {
                ui.separator();
                ui.label(panel_heading("Warnings"));
                for warning in &request.warnings {
                    ui.small(egui::RichText::new(warning).color(TEXT_DIM));
                }
            }
            ui.separator();
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    action = Some(false);
                }
                if ui.button("Move To Trash").clicked() {
                    action = Some(true);
                }
            });
        });

    match action {
        Some(false) => {
            world.resource_mut::<AssetBrowserUiState>().pending_delete = None;
        }
        Some(true) => {
            execute_delete_request(world, &request);
            world.resource_mut::<AssetBrowserUiState>().pending_delete = None;
        }
        None => {}
    }
}

fn execute_delete_request(world: &mut World, request: &AssetDeleteRequest) {
    let trash_root = PathBuf::from("assets/.trash").join(trash_timestamp());
    let mut moved = 0usize;
    let mut errors = Vec::new();
    for file in &request.files {
        let source = Path::new(file);
        if !source.exists() {
            continue;
        }
        match move_file_to_trash(source, &trash_root) {
            Ok(()) => moved += 1,
            Err(error) => errors.push(format!("{file}: {error}")),
        }
    }
    if !errors.is_empty() {
        world.resource_mut::<SceneIo>().status = format!(
            "Could not move {} file(s) to trash: {}",
            errors.len(),
            errors.join("; ")
        );
        return;
    }

    if let Some(parent_path) = request.selection.parent_path().map(str::to_string) {
        if let Some(mut registry) = world.get_resource_mut::<crate::asset_db::AssetRegistry>() {
            registry.records.retain(|record| record.path != parent_path);
            registry.index_dirty = true;
            if let Err(error) = save_registry(&registry) {
                warn!("Asset registry save failed after delete: {error}");
            } else {
                registry.index_dirty = false;
            }
        }
    }

    {
        let mut assets = world.resource_mut::<EditorAssets>();
        if let Some(parent_path) = request.selection.parent_path() {
            if assets
                .selected
                .as_ref()
                .and_then(AssetSelection::parent_path)
                == Some(parent_path)
            {
                assets.selected = None;
            }
        }
        assets.refresh();
    }
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = format!(
        "Moved {} file(s) for {} to {}",
        moved,
        request.label,
        trash_root.to_string_lossy()
    );
}

fn move_file_to_trash(source: &Path, trash_root: &Path) -> Result<(), String> {
    let target = trash_root.join(source);
    if let Some(parent) = target.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
    }
    fs::rename(source, &target)
        .or_else(|_| {
            fs::copy(source, &target)?;
            fs::remove_file(source)
        })
        .map_err(|error| format!("could not move to {}: {error}", target.display()))?;
    Ok(())
}

fn trash_timestamp() -> String {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs().to_string())
        .unwrap_or_else(|_| "now".to_string())
}

fn dedup_strings(values: &mut Vec<String>) {
    let mut seen = HashSet::new();
    values.retain(|value| seen.insert(value.clone()));
}

pub(crate) fn validate_material_conflict_destination(
    world: &World,
    destination: &Path,
) -> Result<String, String> {
    let root = PathBuf::from(&world.resource::<crate::project_io::ProjectWorkspace>().root);
    let root = root.canonicalize().unwrap_or(root);
    let absolute = if destination.is_absolute() {
        destination.to_path_buf()
    } else {
        root.join(destination)
    };
    let absolute = absolute
        .parent()
        .and_then(|parent| parent.canonicalize().ok())
        .and_then(|parent| destination.file_name().map(|name| parent.join(name)))
        .unwrap_or(absolute);
    let relative = absolute.strip_prefix(&root).map_err(|_| {
        format!(
            "Material assets must stay inside {}",
            root.join("assets").display()
        )
    })?;
    if !relative.starts_with("assets") {
        return Err(format!(
            "Material assets must stay inside {}",
            root.join("assets").display()
        ));
    }
    Ok(relative.to_string_lossy().replace('\\', "/"))
}

pub(crate) fn reload_material_after_file_conflict(
    world: &mut World,
    path: &Path,
    intent: &FileWriteIntent,
) -> Result<String, String> {
    let path = path.to_string_lossy();
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        match intent {
            FileWriteIntent::Material => {
                if state
                    .material_draft
                    .as_ref()
                    .is_some_and(|draft| draft.path == path)
                {
                    state.material_draft = None;
                }
            }
            FileWriteIntent::MaterialInstance => {
                if state
                    .material_instance_draft
                    .as_ref()
                    .is_some_and(|draft| draft.path == path)
                {
                    state.material_instance_draft = None;
                }
            }
            _ => return Err("the conflict is not an editable material document".into()),
        }
    }
    world.resource_mut::<EditorAssets>().refresh();
    invalidate_on_catalog_refresh(world);
    Ok(format!("Reloaded {} from disk", path))
}

pub(crate) fn adopt_material_conflict_save_as(
    world: &mut World,
    original_path: &Path,
    catalog_path: String,
    disk_snapshot: FileSnapshot,
    intent: &FileWriteIntent,
) -> Result<String, String> {
    let original_path = original_path.to_string_lossy();
    {
        let mut state = world.resource_mut::<AssetBrowserUiState>();
        match intent {
            FileWriteIntent::Material => {
                if let Some(draft) = state
                    .material_draft
                    .as_mut()
                    .filter(|draft| draft.path == original_path)
                {
                    draft.path.clone_from(&catalog_path);
                    draft.disk_snapshot = disk_snapshot;
                    draft.error = None;
                } else {
                    let asset = MaterialAsset::load_from_path(&catalog_path)?;
                    state.material_draft = Some(MaterialAssetDraft {
                        path: catalog_path.clone(),
                        asset,
                        disk_snapshot,
                        error: None,
                    });
                }
            }
            FileWriteIntent::MaterialInstance => {
                if let Some(draft) = state
                    .material_instance_draft
                    .as_mut()
                    .filter(|draft| draft.path == original_path)
                {
                    draft.path.clone_from(&catalog_path);
                    draft.disk_snapshot = disk_snapshot;
                    draft.error = None;
                } else {
                    let asset = MaterialInstanceAsset::load_from_path(&catalog_path)?;
                    state.material_instance_draft = Some(MaterialInstanceAssetDraft {
                        path: catalog_path.clone(),
                        asset,
                        disk_snapshot,
                        error: None,
                    });
                }
            }
            _ => return Err("the conflict is not an editable material document".into()),
        }
    }
    {
        let mut assets = world.resource_mut::<EditorAssets>();
        assets.refresh();
        assets.select(AssetSelection::File(catalog_path.clone()));
    }
    invalidate_on_catalog_refresh(world);
    Ok(format!("Saved material copy to {catalog_path}"))
}

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

    #[test]
    fn material_apply_refuses_an_external_revision() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-material-external-write-{}",
            uuid::Uuid::new_v4()
        ));
        let path = root.join("assets/materials/test.ron");
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(&path, b"loaded").unwrap();
        let disk_snapshot = FileSnapshot::capture(&path).unwrap();
        let draft = MaterialAssetDraft {
            path: path.to_string_lossy().into_owned(),
            asset: MaterialAsset {
                schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
                label: "Test".into(),
                shader: None,
                shader_ref: None,
                render_state: shared::MaterialRenderState::default(),
                material: MaterialDesc::default(),
            },
            disk_snapshot,
            error: None,
        };
        let state = AssetBrowserUiState {
            material_draft: Some(draft),
            ..Default::default()
        };
        let mut world = World::new();
        world.insert_resource(state);
        world.insert_resource(SceneIo::default());
        fs::write(&path, b"external").unwrap();

        save_material_draft(&mut world);

        assert_eq!(fs::read(&path).unwrap(), b"external");
        assert!(world
            .resource::<AssetBrowserUiState>()
            .material_draft
            .as_ref()
            .and_then(|draft| draft.error.as_deref())
            .is_some_and(|error| error.contains("changed outside Blacksite")));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn audio_filter_matches_only_audio_clips() {
        let audio = EditorAsset {
            label: "Impact".into(),
            path: Some("assets/audio/impact.ogg".into()),
            folder_path: "assets/audio".into(),
            kind: EditorAssetKind::AudioClip,
        };
        let texture = EditorAsset {
            kind: EditorAssetKind::Texture,
            ..audio.clone()
        };

        assert!(asset_matches_kind_filter(&audio, AssetKindFilter::Audio));
        assert!(!asset_matches_kind_filter(&texture, AssetKindFilter::Audio));
        assert_eq!(kind_filter_label(AssetKindFilter::Audio), "Audio");
    }

    #[test]
    fn audio_details_label_supported_formats() {
        assert_eq!(audio_format_label("assets/audio/music.oga"), "Ogg audio");
        assert_eq!(audio_format_label("assets/audio/voice.spx"), "Speex");
        assert_eq!(audio_format_label("assets/audio/source.WAV"), "WAV");
        assert_eq!(audio_format_label("assets/audio/source.mp3"), "MP3");
        assert_eq!(audio_format_label("assets/audio/source.flac"), "FLAC");
    }

    #[test]
    fn material_instance_texture_override_stays_sparse() {
        let first = EditorAssetRef::new("texture-a", "texture:source", "First")
            .with_source_path("assets/textures/first.png");
        let second = EditorAssetRef::new("texture-b", "texture:source", "Second")
            .with_source_path("assets/textures/second.png");
        let mut textures = Vec::new();

        set_material_instance_texture_override(&mut textures, "base_color_texture", Some(first));
        set_material_instance_texture_override(
            &mut textures,
            "base_color_texture",
            Some(second.clone()),
        );

        assert_eq!(textures.len(), 1);
        assert_eq!(textures[0].name, "base_color_texture");
        assert_eq!(textures[0].texture.as_ref(), Some(&second));

        set_material_instance_texture_override(&mut textures, "base_color_texture", None);
        assert!(textures.is_empty());
    }
}
