use crate::ecs::EditorWorld;
use crate::systems::polyhaven::Category;
use crate::systems::retained_ui::{Action, ThumbnailCard, UiHandles};
use nightshade::prelude::*;

const KHRONOS_RECT: Rect = Rect {
    min: Vec2::new(80.0, 80.0),
    max: Vec2::new(560.0, 540.0),
};
const POLYHAVEN_RECT: Rect = Rect {
    min: Vec2::new(120.0, 120.0),
    max: Vec2::new(600.0, 580.0),
};
#[cfg(not(target_arch = "wasm32"))]
const SKETCHFAB_RECT: Rect = Rect {
    min: Vec2::new(160.0, 140.0),
    max: Vec2::new(580.0, 520.0),
};
#[cfg(not(target_arch = "wasm32"))]
const KENNEY_RECT: Rect = Rect {
    min: Vec2::new(140.0, 100.0),
    max: Vec2::new(720.0, 620.0),
};

#[derive(Default, Clone)]
pub struct KhronosHandles {
    pub panel: Entity,
    pub status_label: Entity,
    pub grid_root: Entity,
    pub grid: Entity,
    pub filter_input: Entity,
    pub filter_last: String,
    pub signature: u64,
    pub cards: Vec<ThumbnailCard>,
    pub lookup: std::collections::HashMap<Entity, usize>,
}

#[derive(Default, Clone)]
pub struct PolyhavenHandles {
    pub panel: Entity,
    pub tab_bar: Entity,
    pub filter_input: Entity,
    pub status: Entity,
    pub list_root: Entity,
    pub last_tab: usize,
    pub last_filter: String,
    pub hdris_signature: u64,
    pub models_signature: u64,
    pub list_items: Vec<Entity>,
    pub hdri_lookup: std::collections::HashMap<Entity, usize>,
    pub model_lookup: std::collections::HashMap<Entity, usize>,
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Default, Clone)]
pub struct SketchfabHandles {
    pub panel: Entity,
    pub token_input: Entity,
    pub url_input: Entity,
    pub fetch_button: Entity,
    pub status_label: Entity,
    pub token_link: Entity,
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Default, Clone)]
pub struct KenneyPackSection {
    pub header: Entity,
    pub content: Entity,
    pub entry_indices: Vec<usize>,
    pub built: bool,
    pub last_open: bool,
    pub cards: Vec<ThumbnailCard>,
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Default, Clone)]
pub struct KenneyHandles {
    pub panel: Entity,
    pub status_label: Entity,
    pub root_label: Entity,
    pub pick_directory_button: Entity,
    pub pick_zip_button: Entity,
    pub filter_input: Entity,
    pub tab_bar: Entity,
    pub all_view: Entity,
    pub all_grid_root: Entity,
    pub packs_view: Entity,
    pub packs_root: Entity,
    pub filter_last: String,
    pub root_signature: String,
    pub entries_signature: u64,
    pub last_tab: usize,
    pub last_status_text: String,
    pub last_root_text: String,
    pub cards: Vec<ThumbnailCard>,
    pub lookup: std::collections::HashMap<Entity, usize>,
    pub pack_sections: Vec<KenneyPackSection>,
}

#[derive(Default, Clone)]
pub struct BrowsersHandles {
    pub khronos: KhronosHandles,
    pub polyhaven: PolyhavenHandles,
    #[cfg(not(target_arch = "wasm32"))]
    pub sketchfab: SketchfabHandles,
    #[cfg(not(target_arch = "wasm32"))]
    pub kenney: KenneyHandles,
}

pub fn build(tree: &mut UiTreeBuilder) -> BrowsersHandles {
    let mut handles = BrowsersHandles {
        khronos: build_khronos(tree),
        polyhaven: build_polyhaven(tree),
        #[cfg(not(target_arch = "wasm32"))]
        sketchfab: build_sketchfab(tree),
        #[cfg(not(target_arch = "wasm32"))]
        kenney: build_kenney(tree),
    };
    handles.khronos.signature = u64::MAX;
    handles.polyhaven.last_tab = usize::MAX;
    handles.polyhaven.hdris_signature = u64::MAX;
    handles.polyhaven.models_signature = u64::MAX;
    #[cfg(not(target_arch = "wasm32"))]
    {
        handles.kenney.entries_signature = u64::MAX;
        handles.kenney.last_tab = usize::MAX;
    }
    handles
}

fn build_khronos(tree: &mut UiTreeBuilder) -> KhronosHandles {
    let panel = tree.add_floating_panel("khronos_browser", "Khronos sample assets", KHRONOS_RECT);
    ui_set_visible(tree.world_mut(), panel, false);
    let content = super::panel_content(tree, panel);
    let mut filter_input = Entity::default();
    let mut status_label = Entity::default();
    let mut grid_root = Entity::default();
    tree.in_parent(content, |tree| {
        filter_input = tree.add_text_input("Search assets");
        status_label = browser_status_label(tree, "Loading index...");
        grid_root = browser_grid_root(tree);
    });

    KhronosHandles {
        panel,
        status_label,
        grid_root,
        filter_input,
        ..Default::default()
    }
}

fn browser_grid_root(tree: &mut UiTreeBuilder) -> Entity {
    let wrapper = tree.add_node().fill_width().flex_grow(1.0).entity();
    let scroll = tree.in_parent(wrapper, |tree| tree.add_scroll_area_fill(8.0, 8.0));
    widget::<UiScrollAreaData>(tree.world_mut(), scroll)
        .map(|d| d.content_entity)
        .unwrap_or(scroll)
}

fn build_polyhaven(tree: &mut UiTreeBuilder) -> PolyhavenHandles {
    let panel = tree.add_floating_panel("polyhaven_browser", "Polyhaven", POLYHAVEN_RECT);
    ui_set_visible(tree.world_mut(), panel, false);
    let content = super::panel_content(tree, panel);
    let mut tab_bar = Entity::default();
    let mut filter_input = Entity::default();
    let mut status = Entity::default();
    let mut list_root = Entity::default();
    tree.in_parent(content, |tree| {
        tab_bar = tree.add_tab_bar(&["HDRIs", "Models"], 0);
        filter_input = tree.add_text_input("Search Polyhaven");
        status = browser_status_label(tree, "Loading index...");

        let wrapper = tree.add_node().fill_width().flex_grow(1.0).entity();
        let scroll = tree.in_parent(wrapper, |tree| tree.add_scroll_area_fill(4.0, 2.0));
        list_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
            .map(|d| d.content_entity)
            .unwrap_or(scroll);
    });

    PolyhavenHandles {
        panel,
        tab_bar,
        filter_input,
        status,
        list_root,
        ..Default::default()
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn build_sketchfab(tree: &mut UiTreeBuilder) -> SketchfabHandles {
    let panel = tree.add_floating_panel("sketchfab_browser", "Sketchfab", SKETCHFAB_RECT);
    ui_set_visible(tree.world_mut(), panel, false);
    let content = super::panel_content(tree, panel);

    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size;
    let text_color = theme.text_color;
    let dim = vec4(0.7, 0.7, 0.75, 1.0);

    let mut token_input = Entity::default();
    let mut url_input = Entity::default();
    let mut fetch_button = Entity::default();
    let mut status_label = Entity::default();
    let mut token_link = Entity::default();

    tree.in_parent(content, |tree| {
        tree.add_node()
            .size(100.pct(), (18.0).px())
            .with_text("Sketchfab API token", font * 0.85)
            .text_left()
            .color_raw::<UiBase>(dim)
            .entity();
        token_input = tree.add_text_input("paste your API token");
        ui_text_input_set_password(tree.world_mut(), token_input, true);

        let link_color = vec4(0.49, 0.71, 0.96, 1.0);
        let link_hover = vec4(0.66, 0.84, 1.0, 1.0);
        token_link = tree
            .add_node()
            .size(100.pct(), (18.0).px())
            .with_text("Get a token at sketchfab.com/settings/password", font * 0.8)
            .text_left()
            .color_raw::<UiBase>(link_color)
            .color_raw::<UiHover>(link_hover)
            .with_interaction()
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .entity();

        tree.add_node()
            .size(100.pct(), (18.0).px())
            .with_text("Model URL", font * 0.85)
            .text_left()
            .color_raw::<UiBase>(dim)
            .entity();
        url_input = tree.add_text_input("https://sketchfab.com/3d-models/...");

        fetch_button = tree.add_button("Fetch");

        status_label = tree
            .add_node()
            .size(100.pct(), (18.0).px())
            .with_text("idle", font * 0.85)
            .text_left()
            .color_raw::<UiBase>(text_color)
            .entity();
    });

    SketchfabHandles {
        panel,
        token_input,
        url_input,
        fetch_button,
        status_label,
        token_link,
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn build_kenney(tree: &mut UiTreeBuilder) -> KenneyHandles {
    let panel = tree.add_floating_panel("kenney_browser", "Kenney assets", KENNEY_RECT);
    ui_set_visible(tree.world_mut(), panel, false);
    let content = super::panel_content(tree, panel);

    let mut pick_directory_button = Entity::default();
    let mut pick_zip_button = Entity::default();
    let mut root_label = Entity::default();
    let mut filter_input = Entity::default();
    let mut status_label = Entity::default();
    let mut tab_bar = Entity::default();
    let mut all_view = Entity::default();
    let mut all_grid_root = Entity::default();
    let mut packs_view = Entity::default();
    let mut packs_root = Entity::default();

    tree.in_parent(content, |tree| {
        let row = tree
            .add_node()
            .fill_width()
            .auto_size(AutoSizeMode::Height)
            .flow(FlowDirection::Horizontal, 0.0, 8.0)
            .entity();
        tree.in_parent(row, |tree| {
            pick_directory_button = tree.add_button("Pick directory");
            pick_zip_button = tree.add_button("Pick .zip");
        });
        root_label = browser_status_label(tree, "No Kenney root selected");
        filter_input = tree.add_text_input("Search assets");
        status_label = browser_status_label(tree, "Pick a Kenney directory or .zip to begin");
        tab_bar = tree.add_tab_bar(&["All assets", "Packs"], 0);

        all_view = tree.add_node().fill_width().flex_grow(1.0).entity();
        all_grid_root = tree.in_parent(all_view, |tree| browser_grid_root(tree));

        packs_view = tree.add_node().fill_width().flex_grow(1.0).entity();
        packs_root = tree.in_parent(packs_view, |tree| {
            let wrapper = tree.add_node().fill_width().flex_grow(1.0).entity();
            let scroll = tree.in_parent(wrapper, |tree| tree.add_scroll_area_fill(8.0, 8.0));
            widget::<UiScrollAreaData>(tree.world_mut(), scroll)
                .map(|d| d.content_entity)
                .unwrap_or(scroll)
        });
        ui_set_visible(tree.world_mut(), packs_view, false);
    });

    KenneyHandles {
        panel,
        status_label,
        root_label,
        pick_directory_button,
        pick_zip_button,
        filter_input,
        tab_bar,
        all_view,
        all_grid_root,
        packs_view,
        packs_root,
        ..Default::default()
    }
}

fn browser_status_label(tree: &mut UiTreeBuilder, initial: &str) -> Entity {
    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size;
    let dim = vec4(0.7, 0.7, 0.75, 1.0);
    tree.add_node()
        .size(100.pct(), (18.0).px())
        .with_text(initial, font * 0.85)
        .text_left()
        .color_raw::<UiBase>(dim)
        .entity()
}

pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
    let mut new_actions: Vec<Action> = Vec::new();
    {
        let browsers_handles = &editor_world.resources.ui_handles.browsers;
        for event in ui_events(world) {
            let UiEvent::ButtonClicked(entity) = event else {
                continue;
            };
            if let Some(&index) = browsers_handles.khronos.lookup.get(entity) {
                new_actions.push(Action::LoadKhronosByIndex(index));
                continue;
            }
            if let Some(&index) = browsers_handles.polyhaven.hdri_lookup.get(entity) {
                new_actions.push(Action::LoadPolyhavenByIndex(Category::Hdris, index));
                continue;
            }
            if let Some(&index) = browsers_handles.polyhaven.model_lookup.get(entity) {
                new_actions.push(Action::LoadPolyhavenByIndex(Category::Models, index));
                continue;
            }
            #[cfg(not(target_arch = "wasm32"))]
            if *entity == handles.browsers.sketchfab.token_link {
                open_url_in_browser("https://sketchfab.com/settings/password");
                continue;
            }
            #[cfg(not(target_arch = "wasm32"))]
            if *entity == handles.browsers.sketchfab.fetch_button {
                let token =
                    widget::<UiTextInputData>(world, handles.browsers.sketchfab.token_input)
                        .map(|d| d.text.clone())
                        .unwrap_or_default();
                let url = widget::<UiTextInputData>(world, handles.browsers.sketchfab.url_input)
                    .map(|d| d.text.clone())
                    .unwrap_or_default();
                new_actions.push(Action::SketchfabFetch { token, url });
                continue;
            }
            #[cfg(not(target_arch = "wasm32"))]
            if *entity == handles.browsers.kenney.pick_directory_button {
                new_actions.push(Action::PickKenneyDirectory);
                continue;
            }
            #[cfg(not(target_arch = "wasm32"))]
            if *entity == handles.browsers.kenney.pick_zip_button {
                new_actions.push(Action::PickKenneyZip);
                continue;
            }
            #[cfg(not(target_arch = "wasm32"))]
            if let Some(&index) = browsers_handles.kenney.lookup.get(entity) {
                new_actions.push(Action::LoadKenneyByIndex(index));
                continue;
            }
            let _ = handles;
        }
    }
    editor_world
        .resources
        .ui_interaction
        .actions
        .extend(new_actions);
}

pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    update_khronos(editor_world, world);
    update_polyhaven(editor_world, world);
    #[cfg(not(target_arch = "wasm32"))]
    update_sketchfab(editor_world, world);
    #[cfg(not(target_arch = "wasm32"))]
    update_kenney(editor_world, world);
}

fn build_thumbnail_card(tree: &mut UiTreeBuilder, label_text: &str) -> ThumbnailCard {
    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let bg = theme.input_background_color;
    let hover = theme.background_color_hovered;
    let border = theme.border_color;
    let text_color = theme.text_color;
    let font = theme.font_size;

    let card = tree
        .add_node()
        .size((132.0).px(), (124.0).px())
        .flow(FlowDirection::Vertical, 4.0, 4.0)
        .with_rect(6.0, 1.0, border)
        .color_raw::<UiBase>(bg)
        .color_raw::<UiHover>(hover)
        .with_interaction()
        .with_transition::<UiHover>(10.0, 6.0)
        .with_cursor_icon(winit::window::CursorIcon::Pointer)
        .entity();

    let mut image = Entity::default();
    tree.in_parent(card, |tree| {
        let slot = tree.add_node().size((124.0).px(), (84.0).px()).entity();

        image = tree.in_parent(slot, |tree| {
            tree.add_node()
                .solid(Ab(vec2(1.0, 1.0)), ScalingMode::Fit, vec2(0.0, 0.0))
                .color_raw::<UiBase>(vec4(1.0, 1.0, 1.0, 1.0))
                .entity()
        });

        tree.add_node()
            .size((124.0).px(), (24.0).px())
            .with_text(label_text, font * 0.75)
            .text_center()
            .with_text_overflow(TextOverflow::Clip)
            .color_raw::<UiBase>(text_color)
            .entity();
    });
    ThumbnailCard { card, image }
}

fn refresh_thumbnail_textures(editor_world: &mut EditorWorld, world: &mut World) {
    let cards = editor_world
        .resources
        .ui_handles
        .browsers
        .khronos
        .cards
        .clone();
    let lookup = editor_world
        .resources
        .ui_handles
        .browsers
        .khronos
        .lookup
        .clone();
    let entries = editor_world.resources.browsers.sample_browser.entries();
    let Some(entries) = entries else {
        return;
    };
    let mut dirty = false;
    for card in &cards {
        let Some(&entry_index) = lookup.get(&card.card) else {
            continue;
        };
        let Some(entry) = entries.get(entry_index) else {
            continue;
        };
        let thumb_name = format!("khronos_thumb_{}", entry.name);
        let Some(thumb) = editor_world
            .resources
            .browsers
            .thumbnail_slots
            .get(&thumb_name)
            .copied()
        else {
            continue;
        };
        if patch_thumbnail_image(world, card.image, thumb) {
            dirty = true;
        }
    }
    if dirty {
        ui_mark_render_dirty(world);
    }
}

fn patch_thumbnail_image(
    world: &mut World,
    image_entity: Entity,
    thumb: crate::ecs::ThumbnailSlot,
) -> bool {
    let already_matches = match world.ui.get_ui_node_content(image_entity) {
        Some(UiNodeContent::Image {
            texture_index,
            uv_min,
            uv_max,
        }) => *texture_index == thumb.layer && *uv_min == thumb.uv_min && *uv_max == thumb.uv_max,
        _ => false,
    };
    if already_matches {
        return false;
    }

    if let Some(node) = world.ui.get_ui_layout_node_mut(image_entity) {
        node.base_layout = Some(UiLayoutType::Solid(SolidLayout {
            size: Ab(thumb.aspect).into(),
            scaling: ScalingMode::Fit,
            alignment: vec2(0.0, 0.0),
        }));
    }

    if let Some(content) = world.ui.get_ui_node_content_mut(image_entity) {
        *content = UiNodeContent::Image {
            texture_index: thumb.layer,
            uv_min: thumb.uv_min,
            uv_max: thumb.uv_max,
        };
    }

    ui_mark_layout_dirty(world);
    true
}

fn release_khronos_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
    let slots = std::mem::take(&mut editor_world.resources.browsers.thumbnail_slots);
    for (name, slot) in slots {
        if name.starts_with("khronos_thumb_") {
            ui_release_image_layer(world, slot.layer);
        } else {
            editor_world
                .resources
                .browsers
                .thumbnail_slots
                .insert(name, slot);
        }
    }
    editor_world.resources.browsers.thumbnail_signature = 0;
}

fn build_card_grid(tree_builder: &mut UiTreeBuilder) -> Entity {
    tree_builder
        .add_node()
        .fill_width()
        .auto_size(AutoSizeMode::Height)
        .flow(FlowDirection::Horizontal, 0.0, 8.0)
        .flow_wrap()
        .entity()
}

fn update_khronos(editor_world: &mut EditorWorld, world: &mut World) {
    let entries = editor_world.resources.browsers.sample_browser.entries();
    let signature = entries.as_ref().map(|e| e.len() as u64).unwrap_or(u64::MAX);
    let status_label = editor_world
        .resources
        .ui_handles
        .browsers
        .khronos
        .status_label;
    let grid_root = editor_world.resources.ui_handles.browsers.khronos.grid_root;
    let filter_input = editor_world
        .resources
        .ui_handles
        .browsers
        .khronos
        .filter_input;

    let current_filter = widget::<UiTextInputData>(world, filter_input)
        .map(|data| data.text.clone())
        .unwrap_or_default();
    let filter_changed = current_filter
        != editor_world
            .resources
            .ui_handles
            .browsers
            .khronos
            .filter_last;
    if filter_changed {
        editor_world
            .resources
            .ui_handles
            .browsers
            .khronos
            .filter_last = current_filter.clone();
    }

    if signature != editor_world.resources.ui_handles.browsers.khronos.signature || filter_changed {
        editor_world.resources.ui_handles.browsers.khronos.signature = signature;
        let prior_grid =
            std::mem::take(&mut editor_world.resources.ui_handles.browsers.khronos.grid);
        if prior_grid != Entity::default() {
            despawn_recursive_immediate(world, prior_grid);
        }
        editor_world
            .resources
            .ui_handles
            .browsers
            .khronos
            .cards
            .clear();
        if !filter_changed {
            release_khronos_thumbnails(editor_world, world);
        }
        editor_world
            .resources
            .ui_handles
            .browsers
            .khronos
            .lookup
            .clear();

        match entries {
            Some(entries) => {
                let needle = current_filter.to_lowercase();
                let filtered: Vec<(usize, &crate::systems::sample_assets::AssetEntry)> = entries
                    .iter()
                    .enumerate()
                    .filter(|(_, entry)| {
                        if needle.is_empty() {
                            return true;
                        }
                        entry.label.to_lowercase().contains(&needle)
                            || entry.name.to_lowercase().contains(&needle)
                    })
                    .collect();
                ui_set_text(
                    world,
                    status_label,
                    &format!("{} / {} assets", filtered.len(), entries.len()),
                );
                let mut new_cards: Vec<ThumbnailCard> = Vec::with_capacity(filtered.len());
                let card_indices: Vec<usize> = filtered.iter().map(|(index, _)| *index).collect();
                let card_labels: Vec<String> = filtered
                    .iter()
                    .map(|(_, entry)| entry.label.clone())
                    .collect();
                let new_grid = {
                    let mut tree_builder = UiTreeBuilder::from_parent(world, grid_root);
                    let grid = build_card_grid(&mut tree_builder);
                    tree_builder.in_parent(grid, |tree_builder| {
                        for label in &card_labels {
                            let card = build_thumbnail_card(tree_builder, label);
                            new_cards.push(card);
                        }
                    });
                    tree_builder.finish_subtree();
                    grid
                };
                editor_world.resources.ui_handles.browsers.khronos.grid = new_grid;
                let lookup = &mut editor_world.resources.ui_handles.browsers.khronos.lookup;
                for (card, original_index) in new_cards.iter().zip(card_indices.iter()) {
                    lookup.insert(card.card, *original_index);
                }
                editor_world.resources.ui_handles.browsers.khronos.cards = new_cards;
            }
            None => {
                let text = match editor_world.resources.browsers.sample_browser.index_error() {
                    Some(error) => format!("failed: {error}"),
                    None => "Loading index...".to_string(),
                };
                ui_set_text(world, status_label, &text);
            }
        }
    }

    refresh_thumbnail_textures(editor_world, world);

    if editor_world
        .resources
        .browsers
        .sample_browser
        .entries()
        .is_none()
        && let Some(error) = editor_world.resources.browsers.sample_browser.index_error()
    {
        ui_set_text(world, status_label, &format!("failed: {error}"));
    }
}

fn update_polyhaven(editor_world: &mut EditorWorld, world: &mut World) {
    let tab_bar = editor_world.resources.ui_handles.browsers.polyhaven.tab_bar;
    let filter_input = editor_world
        .resources
        .ui_handles
        .browsers
        .polyhaven
        .filter_input;
    let status_label = editor_world.resources.ui_handles.browsers.polyhaven.status;
    let list_root = editor_world
        .resources
        .ui_handles
        .browsers
        .polyhaven
        .list_root;

    let active_tab = widget::<UiTabBarData>(world, tab_bar)
        .map(|data| data.selected_tab)
        .unwrap_or(0);
    let filter_text = widget::<UiTextInputData>(world, filter_input)
        .map(|data| data.text.clone())
        .unwrap_or_default();
    let category = if active_tab == 0 {
        Category::Hdris
    } else {
        Category::Models
    };

    let entries = editor_world
        .resources
        .browsers
        .polyhaven_browser
        .entries(category);
    let entry_signature = entries.as_ref().map(|e| e.len() as u64).unwrap_or(u64::MAX);
    let prior_signature = match category {
        Category::Hdris => {
            editor_world
                .resources
                .ui_handles
                .browsers
                .polyhaven
                .hdris_signature
        }
        Category::Models => {
            editor_world
                .resources
                .ui_handles
                .browsers
                .polyhaven
                .models_signature
        }
    };
    let tab_changed = active_tab
        != editor_world
            .resources
            .ui_handles
            .browsers
            .polyhaven
            .last_tab;
    let filter_changed = filter_text
        != editor_world
            .resources
            .ui_handles
            .browsers
            .polyhaven
            .last_filter;
    let signature_changed = entry_signature != prior_signature;

    if !tab_changed && !filter_changed && !signature_changed {
        if entries.is_none()
            && let Some(error) = editor_world
                .resources
                .browsers
                .polyhaven_browser
                .index_error(category)
        {
            ui_set_text(world, status_label, &format!("failed: {error}"));
        }
        return;
    }

    editor_world
        .resources
        .ui_handles
        .browsers
        .polyhaven
        .last_tab = active_tab;
    editor_world
        .resources
        .ui_handles
        .browsers
        .polyhaven
        .last_filter = filter_text.clone();
    match category {
        Category::Hdris => {
            editor_world
                .resources
                .ui_handles
                .browsers
                .polyhaven
                .hdris_signature = entry_signature
        }
        Category::Models => {
            editor_world
                .resources
                .ui_handles
                .browsers
                .polyhaven
                .models_signature = entry_signature
        }
    }

    let prior = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .browsers
            .polyhaven
            .list_items,
    );
    for entity in prior {
        despawn_recursive_immediate(world, entity);
    }
    {
        let polyhaven_handles = &mut editor_world.resources.ui_handles.browsers.polyhaven;
        polyhaven_handles.hdri_lookup.clear();
        polyhaven_handles.model_lookup.clear();
    }

    match entries {
        Some(entries) => {
            let needle = filter_text.to_lowercase();
            const MAX_DISPLAY: usize = 200;
            let filtered: Vec<(usize, &crate::systems::polyhaven::AssetEntry)> = entries
                .iter()
                .enumerate()
                .filter(|(_, entry)| {
                    if needle.is_empty() {
                        return true;
                    }
                    entry.name.to_lowercase().contains(&needle)
                        || entry.slug.to_lowercase().contains(&needle)
                })
                .take(MAX_DISPLAY)
                .collect();

            let total_matches = if needle.is_empty() {
                entries.len()
            } else {
                entries
                    .iter()
                    .filter(|entry| {
                        entry.name.to_lowercase().contains(&needle)
                            || entry.slug.to_lowercase().contains(&needle)
                    })
                    .count()
            };
            let shown = filtered.len();
            ui_set_text(
                world,
                status_label,
                &format!(
                    "{shown} of {total_matches} matches ({} total)",
                    entries.len()
                ),
            );

            let row_indices: Vec<usize> = filtered.iter().map(|(index, _)| *index).collect();
            let row_labels: Vec<String> = filtered
                .iter()
                .map(|(_, entry)| entry.name.clone())
                .collect();
            let mut new_items: Vec<Entity> = Vec::with_capacity(filtered.len());
            {
                let mut tree_builder = UiTreeBuilder::from_parent(world, list_root);
                for label in &row_labels {
                    let row = build_polyhaven_row(&mut tree_builder, label);
                    new_items.push(row);
                }
                tree_builder.finish_subtree();
            }
            let target = match category {
                Category::Hdris => {
                    &mut editor_world
                        .resources
                        .ui_handles
                        .browsers
                        .polyhaven
                        .hdri_lookup
                }
                Category::Models => {
                    &mut editor_world
                        .resources
                        .ui_handles
                        .browsers
                        .polyhaven
                        .model_lookup
                }
            };
            for (row, original_index) in new_items.iter().zip(row_indices.iter()) {
                target.insert(*row, *original_index);
            }
            editor_world
                .resources
                .ui_handles
                .browsers
                .polyhaven
                .list_items = new_items;
        }
        None => {
            let text = match editor_world
                .resources
                .browsers
                .polyhaven_browser
                .index_error(category)
            {
                Some(error) => format!("failed: {error}"),
                None => "Loading index...".to_string(),
            };
            ui_set_text(world, status_label, &text);
        }
    }
}

fn build_polyhaven_row(tree: &mut UiTreeBuilder, label: &str) -> Entity {
    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size;
    tree.add_node()
        .size(100.pct(), (24.0).px())
        .rect(2.0)
        .color_raw::<UiBase>(vec4(0.0, 0.0, 0.0, 0.0))
        .fg_hover(ThemeColor::BackgroundHover)
        .with_interaction()
        .with_transition::<UiHover>(8.0, 6.0)
        .with_cursor_icon(winit::window::CursorIcon::Pointer)
        .with_children(|tree| {
            tree.add_node()
                .window(
                    Ab(vec2(8.0, 0.0)),
                    Rl(vec2(100.0, 100.0)) + Ab(vec2(-8.0, 0.0)),
                    Anchor::TopLeft,
                )
                .with_text(label, font * 0.85)
                .text_left()
                .with_text_overflow(TextOverflow::Ellipsis)
                .fg(ThemeColor::Text)
                .entity();
        })
        .entity()
}

#[cfg(not(target_arch = "wasm32"))]
fn build_pack_sections(
    world: &mut World,
    parent: Entity,
    entries: &[crate::systems::kenney_assets::KenneyEntry],
    needle: &str,
) -> Vec<KenneyPackSection> {
    let mut by_pack: std::collections::BTreeMap<(String, String), Vec<usize>> =
        std::collections::BTreeMap::new();
    for (index, entry) in entries.iter().enumerate() {
        if !needle.is_empty()
            && !entry.display_name.to_lowercase().contains(needle)
            && !entry.pack.to_lowercase().contains(needle)
        {
            continue;
        }
        by_pack
            .entry((entry.category.clone(), entry.pack.clone()))
            .or_default()
            .push(index);
    }

    let mut sections = Vec::with_capacity(by_pack.len());
    let mut tree = UiTreeBuilder::from_parent(world, parent);
    for ((category, pack), indices) in by_pack {
        let count = indices.len();
        let label = if category.is_empty() {
            format!("{pack}  ({count})")
        } else {
            format!("{pack}  ({count})  - {category}")
        };
        let header = tree.add_collapsing_header(&label, false);
        let content = widget::<UiCollapsingHeaderData>(tree.world_mut(), header)
            .map(|d| d.content_entity)
            .unwrap_or(header);
        sections.push(KenneyPackSection {
            header,
            content,
            entry_indices: indices,
            built: false,
            last_open: false,
            cards: Vec::new(),
        });
    }
    tree.finish_subtree();
    sections
}

#[cfg(not(target_arch = "wasm32"))]
fn sync_pack_sections(editor_world: &mut EditorWorld, world: &mut World) {
    let entries = match editor_world.resources.browsers.kenney_browser.entries() {
        Some(entries) => entries,
        None => return,
    };
    let mut sections = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .pack_sections,
    );
    let mut lookup_changes: Vec<(Entity, usize)> = Vec::new();
    let mut lookup_removals: Vec<Entity> = Vec::new();
    let mut new_cards: Vec<ThumbnailCard> = Vec::new();

    for section in &mut sections {
        let open = widget::<UiCollapsingHeaderData>(world, section.header)
            .map(|data| data.open)
            .unwrap_or(false);
        if open == section.last_open && open == section.built {
            continue;
        }
        section.last_open = open;
        if open && !section.built {
            let mut tree = UiTreeBuilder::from_parent(world, section.content);
            let grid = build_card_grid(&mut tree);
            tree.in_parent(grid, |tree| {
                for &entry_index in &section.entry_indices {
                    let label = entries[entry_index].display_name.clone();
                    let card = build_thumbnail_card(tree, &label);
                    lookup_changes.push((card.card, entry_index));
                    section.cards.push(card);
                    new_cards.push(card);
                }
            });
            tree.finish_subtree();
            section.built = true;
        } else if !open && section.built {
            for card in section.cards.drain(..) {
                lookup_removals.push(card.card);
                despawn_recursive_immediate(world, card.card);
            }
            section.built = false;
        }
    }

    let kenney = &mut editor_world.resources.ui_handles.browsers.kenney;
    kenney.pack_sections = sections;
    for entity in lookup_removals {
        kenney.lookup.remove(&entity);
        kenney.cards.retain(|card| card.card != entity);
    }
    for (entity, index) in lookup_changes {
        kenney.lookup.insert(entity, index);
    }
    kenney.cards.extend(new_cards);
}

#[cfg(not(target_arch = "wasm32"))]
fn update_kenney(editor_world: &mut EditorWorld, world: &mut World) {
    let entries = editor_world.resources.browsers.kenney_browser.entries();
    let entry_signature = entries.as_ref().map(|e| e.len() as u64).unwrap_or(u64::MAX);
    let root_signature = editor_world
        .resources
        .browsers
        .kenney_browser
        .root()
        .map(|path| path.display().to_string())
        .unwrap_or_default();
    let status_text = editor_world.resources.browsers.kenney_browser.status_text();

    let status_label = editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .status_label;
    let root_label = editor_world.resources.ui_handles.browsers.kenney.root_label;
    let all_view = editor_world.resources.ui_handles.browsers.kenney.all_view;
    let all_grid_root = editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .all_grid_root;
    let packs_view = editor_world.resources.ui_handles.browsers.kenney.packs_view;
    let packs_root = editor_world.resources.ui_handles.browsers.kenney.packs_root;
    let tab_bar = editor_world.resources.ui_handles.browsers.kenney.tab_bar;
    let filter_input = editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .filter_input;

    if status_text
        != editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .last_status_text
    {
        ui_set_text(world, status_label, &status_text);
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .last_status_text = status_text;
    }
    let root_display = if root_signature.is_empty() {
        "No Kenney root selected".to_string()
    } else {
        format!("Root: {root_signature}")
    };
    if root_display
        != editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .last_root_text
    {
        ui_set_text(world, root_label, &root_display);
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .last_root_text = root_display;
    }

    let current_filter = widget::<UiTextInputData>(world, filter_input)
        .map(|data| data.text.clone())
        .unwrap_or_default();
    let active_tab = widget::<UiTabBarData>(world, tab_bar)
        .map(|data| data.selected_tab)
        .unwrap_or(0);
    let filter_changed = current_filter
        != editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .filter_last;
    let root_changed = root_signature
        != editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .root_signature;
    let entries_changed = entry_signature
        != editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .entries_signature;
    let tab_changed = active_tab != editor_world.resources.ui_handles.browsers.kenney.last_tab;

    if filter_changed {
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .filter_last = current_filter.clone();
    }
    if root_changed {
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .root_signature = root_signature;
    }
    if tab_changed {
        editor_world.resources.ui_handles.browsers.kenney.last_tab = active_tab;
        ui_set_visible(world, all_view, active_tab == 0);
        ui_set_visible(world, packs_view, active_tab == 1);
    }

    if !entries_changed && !filter_changed && !root_changed && !tab_changed {
        sync_pack_sections(editor_world, world);
        refresh_kenney_thumbnails(editor_world, world);
        return;
    }

    if entries_changed {
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .entries_signature = entry_signature;
        if root_changed {
            release_kenney_thumbnails(editor_world, world);
        }
    }

    let prior_cards = std::mem::take(&mut editor_world.resources.ui_handles.browsers.kenney.cards);
    for card in prior_cards {
        despawn_recursive_immediate(world, card.card);
    }
    let prior_sections = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .pack_sections,
    );
    for section in prior_sections {
        despawn_recursive_immediate(world, section.header);
    }
    editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .lookup
        .clear();

    let Some(entries) = entries else {
        return;
    };

    let needle = current_filter.to_lowercase();

    if active_tab == 0 {
        const MAX_DISPLAY: usize = 200;
        let filtered: Vec<(usize, &crate::systems::kenney_assets::KenneyEntry)> = entries
            .iter()
            .enumerate()
            .filter(|(_, entry)| {
                if needle.is_empty() {
                    return true;
                }
                entry.display_name.to_lowercase().contains(&needle)
                    || entry.pack.to_lowercase().contains(&needle)
            })
            .take(MAX_DISPLAY)
            .collect();
        let card_indices: Vec<usize> = filtered.iter().map(|(index, _)| *index).collect();
        let card_labels: Vec<String> = filtered
            .iter()
            .map(|(_, entry)| entry.display_name.clone())
            .collect();
        let mut new_cards: Vec<ThumbnailCard> = Vec::with_capacity(card_labels.len());
        {
            let mut tree_builder = UiTreeBuilder::from_parent(world, all_grid_root);
            let grid = build_card_grid(&mut tree_builder);
            tree_builder.in_parent(grid, |tree_builder| {
                for label in &card_labels {
                    let card = build_thumbnail_card(tree_builder, label);
                    new_cards.push(card);
                }
            });
            tree_builder.finish_subtree();
        }
        let lookup = &mut editor_world.resources.ui_handles.browsers.kenney.lookup;
        for (card, original_index) in new_cards.iter().zip(card_indices.iter()) {
            lookup.insert(card.card, *original_index);
        }
        editor_world.resources.ui_handles.browsers.kenney.cards = new_cards;
    } else {
        let sections = build_pack_sections(world, packs_root, entries.as_ref(), &needle);
        editor_world
            .resources
            .ui_handles
            .browsers
            .kenney
            .pack_sections = sections;
    }

    sync_pack_sections(editor_world, world);
    refresh_kenney_thumbnails(editor_world, world);
}

#[cfg(not(target_arch = "wasm32"))]
fn refresh_kenney_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
    let cards = editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .cards
        .clone();
    let lookup = editor_world
        .resources
        .ui_handles
        .browsers
        .kenney
        .lookup
        .clone();
    let Some(entries) = editor_world.resources.browsers.kenney_browser.entries() else {
        return;
    };
    let mut dirty = false;
    for card in &cards {
        let Some(&entry_index) = lookup.get(&card.card) else {
            continue;
        };
        let Some(entry) = entries.get(entry_index) else {
            continue;
        };
        let thumb_name = format!("kenney_thumb_{}", entry.asset_name);
        let Some(thumb) = editor_world
            .resources
            .browsers
            .thumbnail_slots
            .get(&thumb_name)
            .copied()
        else {
            continue;
        };
        if patch_thumbnail_image(world, card.image, thumb) {
            dirty = true;
        }
    }
    if dirty {
        ui_mark_render_dirty(world);
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn release_kenney_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
    let slots = std::mem::take(&mut editor_world.resources.browsers.thumbnail_slots);
    for (name, slot) in slots {
        if name.starts_with("kenney_thumb_") {
            ui_release_image_layer(world, slot.layer);
        } else {
            editor_world
                .resources
                .browsers
                .thumbnail_slots
                .insert(name, slot);
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn update_sketchfab(editor_world: &EditorWorld, world: &mut World) {
    use crate::systems::sketchfab::FetchStatus;
    let status = editor_world.resources.browsers.sketchfab_browser.status();
    let label = editor_world
        .resources
        .ui_handles
        .browsers
        .sketchfab
        .status_label;
    let text = match status {
        FetchStatus::Idle => "idle".to_string(),
        FetchStatus::Working(message) => format!("working: {message}"),
        FetchStatus::Failed(message) => format!("failed: {message}"),
    };
    ui_set_text(world, label, &text);
}
