use nalgebra_glm::Vec2;

use crate::ecs::ui::components::UiDepthMode;
use crate::ecs::ui::layout_types::{FlowLayout, compute_layout_rect};
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::units::UiEvalContext;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::UiLayer;

use super::color_blend::apply_animation_to_rect;

struct StackEntry {
    entity: freecs::Entity,
    parent_rect: Rect,
    parent_depth: f32,
    parent_font_size: f32,
    parent_clip_rect: Option<Rect>,
    parent_layer: Option<UiLayer>,
    flow_override_rect: Option<Rect>,
}

fn compute_window_dpi_signature(world: &World) -> f32 {
    world.resources.window.cached_scale_factor
}

pub fn ui_docked_panel_layout_system(world: &mut World) {
    if !world.resources.retained_ui.enabled {
        return;
    }

    let primary_viewport_size = world
        .resources
        .window
        .cached_viewport_size
        .map(|(width, height)| Vec2::new(width as f32, height as f32))
        .unwrap_or(Vec2::new(800.0, 600.0));
    let primary_dpi_scale = world.resources.window.cached_scale_factor;

    world.resources.retained_ui.dock_state = crate::ecs::ui::resources::DockState::default();

    let panel_entities: Vec<freecs::Entity> = world
        .ui
        .query_entities(crate::ecs::world::UI_PANEL)
        .collect();

    type PanelEntry = (
        freecs::Entity,
        crate::ecs::ui::components::UiPanelKind,
        f32,
        i32,
        bool,
        bool,
        String,
        String,
        Option<[f32; 2]>,
        Option<[f32; 2]>,
    );
    let mut entries: Vec<PanelEntry> = Vec::new();

    for entity in &panel_entities {
        let visible = world
            .ui
            .get_ui_layout_node(*entity)
            .map(|node| node.visible)
            .unwrap_or(true);
        let Some(data) = world.ui.get_ui_panel(*entity) else {
            continue;
        };
        let floating_position =
            if data.panel_kind == crate::ecs::ui::components::UiPanelKind::Floating {
                world
                    .ui
                    .get_ui_layout_node(*entity)
                    .and_then(|node| node.base_layout)
                    .and_then(|layout| match layout {
                        crate::ecs::ui::layout_types::UiLayoutType::Window(window) => {
                            window.position.absolute
                        }
                        _ => None,
                    })
                    .map(|ab| [ab.x, ab.y])
            } else {
                None
            };
        let floating_size = if data.panel_kind == crate::ecs::ui::components::UiPanelKind::Floating
        {
            world
                .ui
                .get_ui_layout_node(*entity)
                .and_then(|node| node.base_layout)
                .and_then(|layout| match layout {
                    crate::ecs::ui::layout_types::UiLayoutType::Window(window) => {
                        window.size.absolute
                    }
                    _ => None,
                })
                .map(|ab| [ab.x, ab.y])
        } else {
            None
        };
        entries.push((
            *entity,
            data.panel_kind,
            data.default_dock_size,
            data.focus_order,
            visible,
            data.collapsed,
            data.id.clone(),
            data.title.clone(),
            floating_position,
            floating_size,
        ));
    }

    entries.sort_by_key(|entry| entry.3);

    let window_viewport_size = primary_viewport_size;
    let window_dpi_scale = primary_dpi_scale;
    let mut reserved = crate::ecs::ui::resources::ReservedAreas::default();
    let mut configs: Vec<crate::ecs::ui::resources::PanelDockConfig> = Vec::new();

    for entry in &entries {
        let (
            entity,
            panel_kind,
            dock_size,
            focus_order,
            visible,
            collapsed,
            id,
            title,
            floating_position,
            floating_size,
        ) = entry;
        configs.push(crate::ecs::ui::resources::PanelDockConfig {
            id: id.clone(),
            title: title.clone(),
            kind: *panel_kind,
            focus_order: *focus_order,
            size: *dock_size,
            visible: *visible,
            collapsed: *collapsed,
            floating_position: *floating_position,
            floating_size: *floating_size,
        });

        if !visible {
            continue;
        }
        let (position, size) = match panel_kind {
            crate::ecs::ui::components::UiPanelKind::DockedLeft => {
                let h = window_viewport_size.y - reserved.top - reserved.bottom;
                (
                    Vec2::new(reserved.left, reserved.top),
                    Vec2::new(dock_size * window_dpi_scale, h),
                )
            }
            crate::ecs::ui::components::UiPanelKind::DockedRight => {
                let h = window_viewport_size.y - reserved.top - reserved.bottom;
                (
                    Vec2::new(
                        window_viewport_size.x - reserved.right - dock_size * window_dpi_scale,
                        reserved.top,
                    ),
                    Vec2::new(dock_size * window_dpi_scale, h),
                )
            }
            crate::ecs::ui::components::UiPanelKind::DockedTop => {
                let w = window_viewport_size.x - reserved.left - reserved.right;
                (
                    Vec2::new(reserved.left, reserved.top),
                    Vec2::new(w, dock_size * window_dpi_scale),
                )
            }
            crate::ecs::ui::components::UiPanelKind::DockedBottom => {
                let w = window_viewport_size.x - reserved.left - reserved.right;
                (
                    Vec2::new(
                        reserved.left,
                        window_viewport_size.y - reserved.bottom - dock_size * window_dpi_scale,
                    ),
                    Vec2::new(w, dock_size * window_dpi_scale),
                )
            }
            crate::ecs::ui::components::UiPanelKind::Floating => continue,
        };

        if let Some(node) = world.ui.get_ui_layout_node_mut(*entity)
            && let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                node.base_layout.as_mut()
        {
            let current_ab = window.position.absolute.unwrap_or(Vec2::new(0.0, 0.0));
            let parent_min = node.computed_rect.min - current_ab * window_dpi_scale;
            window.position =
                crate::ecs::ui::units::Ab((position - parent_min) / window_dpi_scale).into();
            window.size = crate::ecs::ui::units::Ab(size / window_dpi_scale).into();
        }

        match panel_kind {
            crate::ecs::ui::components::UiPanelKind::DockedLeft => {
                reserved.left += dock_size * window_dpi_scale;
            }
            crate::ecs::ui::components::UiPanelKind::DockedRight => {
                reserved.right += dock_size * window_dpi_scale;
            }
            crate::ecs::ui::components::UiPanelKind::DockedTop => {
                reserved.top += dock_size * window_dpi_scale;
            }
            crate::ecs::ui::components::UiPanelKind::DockedBottom => {
                reserved.bottom += dock_size * window_dpi_scale;
            }
            crate::ecs::ui::components::UiPanelKind::Floating => {}
        }
    }

    world.resources.retained_ui.dock_state.primary = crate::ecs::ui::resources::WindowDockLayout {
        reserved_areas: reserved,
        panels: configs,
    };
}

pub fn ui_layout_compute_system(world: &mut World) {
    if !world.resources.retained_ui.enabled {
        return;
    }

    let viewport_raw = world.resources.window.cached_viewport_size;
    let dpi_scale = world.resources.window.cached_scale_factor;
    let text_generation = world.resources.text.cache.generation;

    let combined_window_dpi_signature = compute_window_dpi_signature(world);

    let viewport_changed = world.resources.retained_ui.dirty.last_compute_viewport != viewport_raw;
    let dpi_changed =
        (combined_window_dpi_signature - world.resources.retained_ui.dirty.last_compute_dpi).abs()
            > f32::EPSILON;
    let text_changed = text_generation
        != world
            .resources
            .retained_ui
            .dirty
            .last_compute_text_generation;
    let layout_dirty = world.resources.retained_ui.dirty.layout_dirty;

    if !viewport_changed && !dpi_changed && !text_changed && !layout_dirty {
        return;
    }

    world.resources.retained_ui.dirty.last_compute_viewport = viewport_raw;
    world.resources.retained_ui.dirty.last_compute_dpi = combined_window_dpi_signature;
    world
        .resources
        .retained_ui
        .dirty
        .last_compute_text_generation = text_generation;
    world.resources.retained_ui.dirty.layout_dirty = false;
    world.resources.retained_ui.dirty.render_dirty = true;
    world.resources.retained_ui.dirty.global_version += 1;

    prune_taffy(world);

    let viewport_size = viewport_raw
        .map(|(width, height)| Vec2::new(width as f32, height as f32))
        .unwrap_or(Vec2::new(800.0, 600.0));

    let root_entities: Vec<freecs::Entity> = world
        .ui
        .query_entities(crate::ecs::world::UI_LAYOUT_ROOT)
        .collect();

    let mut region_rects = std::mem::take(&mut world.resources.retained_ui.taffy_region_rects);
    region_rects.clear();

    let mut children_scratch: Vec<freecs::Entity> = Vec::new();

    let mut all_sorted_nodes: Vec<(freecs::Entity, f32)> = Vec::new();

    for root_entity in root_entities {
        let (root_intrinsic_scale, default_font_size) = {
            match world.ui.get_ui_layout_root(root_entity) {
                Some(root) => (root.absolute_scale, root.default_font_size),
                None => continue,
            }
        };

        let (root_viewport_size, root_window_dpi) = (viewport_size, dpi_scale);
        let absolute_scale = root_intrinsic_scale * root_window_dpi;

        let root_rect = Rect::new(0.0, 0.0, root_viewport_size.x, root_viewport_size.y);
        let root_children: &[freecs::Entity] = world
            .resources
            .transform_state
            .children_cache
            .get(&root_entity)
            .map(|v| v.as_slice())
            .unwrap_or(&[]);

        let mut stack: Vec<StackEntry> = Vec::new();

        for child in root_children.iter().rev() {
            if world.ui.get_ui_layout_node(*child).is_some() {
                stack.push(StackEntry {
                    entity: *child,
                    parent_rect: root_rect,
                    parent_depth: 0.0,
                    parent_font_size: default_font_size,
                    parent_clip_rect: None,
                    parent_layer: None,
                    flow_override_rect: None,
                });
            }
        }

        while let Some(entry) = stack.pop() {
            let (
                base_layout,
                flow_layout,
                responsive_flow,
                grid_layout,
                depth_mode,
                node_font_size,
                visible,
                clip_content,
                node_layer,
                node_min_size,
                node_max_size,
            ) = {
                match world.ui.get_ui_layout_node(entry.entity) {
                    Some(node) => (
                        node.base_layout,
                        node.flow_layout,
                        node.responsive_flow,
                        node.grid_layout,
                        node.depth,
                        node.font_size,
                        node.visible,
                        node.clip_content,
                        node.layer,
                        node.min_size,
                        node.max_size,
                    ),
                    None => continue,
                }
            };

            if !visible {
                continue;
            }

            let font_size = node_font_size.unwrap_or(entry.parent_font_size);

            let context = UiEvalContext {
                parent_width: entry.parent_rect.width(),
                parent_height: entry.parent_rect.height(),
                viewport_width: root_viewport_size.x,
                viewport_height: root_viewport_size.y,
                font_size,
                absolute_scale,
            };

            let computed_rect = if let Some(flow_rect) = entry.flow_override_rect {
                flow_rect
            } else if let Some(base_layout) = &base_layout {
                compute_layout_rect(base_layout, &context, &entry.parent_rect)
            } else {
                entry.parent_rect
            };

            let computed_rect = if node_min_size.is_some() || node_max_size.is_some() {
                let mut width = computed_rect.width();
                let mut height = computed_rect.height();
                if let Some(min) = node_min_size {
                    width = width.max(min.x);
                    height = height.max(min.y);
                }
                if let Some(max) = node_max_size {
                    width = width.min(max.x);
                    height = height.min(max.y);
                }
                Rect::from_min_max(
                    computed_rect.min,
                    Vec2::new(computed_rect.min.x + width, computed_rect.min.y + height),
                )
            } else {
                computed_rect
            };

            let depth = match depth_mode {
                UiDepthMode::Add(delta) => entry.parent_depth + delta,
                UiDepthMode::Set(absolute) => absolute,
            };

            let effective_layer = node_layer.or(entry.parent_layer);

            let is_popup_layer = matches!(
                effective_layer,
                Some(UiLayer::Popups) | Some(UiLayer::Tooltips)
            );

            let (node_clip_rect, child_clip_rect) = if is_popup_layer {
                (None, None)
            } else if clip_content {
                let clipped = match &entry.parent_clip_rect {
                    Some(parent_clip) => {
                        Some(computed_rect.intersect(parent_clip).unwrap_or(*parent_clip))
                    }
                    None => Some(computed_rect),
                };
                (clipped, clipped)
            } else {
                (entry.parent_clip_rect, entry.parent_clip_rect)
            };

            let computed_rect = if let Some(node) = world.ui.get_ui_layout_node(entry.entity)
                && let Some(animation) = &node.animation
            {
                apply_animation_to_rect(computed_rect, animation)
            } else {
                computed_rect
            };

            let prev_rect_data = world
                .ui
                .get_ui_layout_node(entry.entity)
                .map(|n| (n.computed_rect.min, n.computed_rect.max));
            let new_rect_data = (computed_rect.min, computed_rect.max);
            if let Some(node) = world.ui.get_ui_layout_node_mut(entry.entity) {
                node.computed_rect = computed_rect;
                node.computed_depth = depth;
                node.computed_font_scale = root_intrinsic_scale;
                node.computed_clip_rect = node_clip_rect;
                node.computed_layer = effective_layer;
            }
            if prev_rect_data != Some(new_rect_data) {
                let global = world.resources.retained_ui.dirty.global_version;
                world
                    .resources
                    .retained_ui
                    .dirty
                    .node_versions
                    .insert(entry.entity, global);
            }

            all_sorted_nodes.push((entry.entity, depth));

            children_scratch.clear();
            if let Some(cached_children) = world
                .resources
                .transform_state
                .children_cache
                .get(&entry.entity)
            {
                children_scratch.extend_from_slice(cached_children);
            }

            let scroll_offset = world
                .ui
                .get_ui_layout_node(entry.entity)
                .map(|n| n.scroll_offset * root_window_dpi)
                .unwrap_or(Vec2::new(0.0, 0.0));
            let scroll_shift = scroll_offset != Vec2::new(0.0, 0.0);
            let scrolled_parent_rect = if scroll_shift {
                Rect::from_min_max(
                    computed_rect.min - scroll_offset,
                    computed_rect.max - scroll_offset,
                )
            } else {
                computed_rect
            };

            if flow_layout.is_some() || grid_layout.is_some() {
                let is_region_root = entry.flow_override_rect.is_none();
                let responsive_repass = flow_layout.is_some() && responsive_flow.is_some();
                let child_rects: Vec<(freecs::Entity, Option<Rect>)> =
                    if is_region_root || responsive_repass {
                        let parent_style = if let Some(flow) = &flow_layout {
                            let effective_flow = if let Some(resp) = responsive_flow
                                && computed_rect.width() < resp.max_width * absolute_scale
                            {
                                FlowLayout {
                                    direction: resp.direction,
                                    ..*flow
                                }
                            } else {
                                *flow
                            };
                            crate::ecs::ui::taffy_layout::flow_to_style(
                                world.ui.get_ui_layout_node(entry.entity).unwrap(),
                                &effective_flow,
                                scrolled_parent_rect.size(),
                                absolute_scale,
                            )
                        } else {
                            crate::ecs::ui::taffy_layout::grid_to_style(
                                world.ui.get_ui_layout_node(entry.entity).unwrap(),
                                grid_layout.as_ref().unwrap(),
                                scrolled_parent_rect.size(),
                                absolute_scale,
                            )
                        };
                        compute_taffy_children(
                            world,
                            entry.entity,
                            parent_style,
                            &children_scratch,
                            &scrolled_parent_rect,
                            absolute_scale,
                            &mut region_rects,
                        )
                    } else {
                        read_region_children(world, &children_scratch, &region_rects)
                    };

                for (child, child_rect) in child_rects.iter().rev() {
                    stack.push(StackEntry {
                        entity: *child,
                        parent_rect: scrolled_parent_rect,
                        parent_depth: depth,
                        parent_font_size: font_size,
                        parent_clip_rect: child_clip_rect,
                        parent_layer: effective_layer,
                        flow_override_rect: *child_rect,
                    });
                }
            } else {
                for child in children_scratch.iter().rev() {
                    if world.ui.get_ui_layout_node(*child).is_some() {
                        stack.push(StackEntry {
                            entity: *child,
                            parent_rect: scrolled_parent_rect,
                            parent_depth: depth,
                            parent_font_size: font_size,
                            parent_clip_rect: child_clip_rect,
                            parent_layer: effective_layer,
                            flow_override_rect: None,
                        });
                    }
                }
            }
        }
    }

    world.resources.retained_ui.taffy_region_rects = region_rects;

    all_sorted_nodes.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
    let primary_viewport = (viewport_size.x, viewport_size.y);
    let primary_grid =
        crate::ecs::ui::picking::build_picking_grid(world, &all_sorted_nodes, primary_viewport);
    world.resources.retained_ui.z_sorted_nodes = all_sorted_nodes;
    world.resources.retained_ui.picking_grid = Some(primary_grid);
}

fn compute_taffy_children(
    world: &mut World,
    parent_entity: freecs::Entity,
    parent_style: taffy::Style,
    children: &[freecs::Entity],
    parent_rect: &Rect,
    dpi_scale: f32,
    region_rects: &mut std::collections::HashMap<freecs::Entity, Rect>,
) -> Vec<(freecs::Entity, Option<Rect>)> {
    use crate::ecs::ui::taffy_layout::available_size_for;

    let parent_size = parent_rect.size();

    world
        .resources
        .retained_ui
        .taffy
        .ensure_leaf(parent_entity, parent_style);

    let mut child_node_ids: Vec<taffy::NodeId> = Vec::with_capacity(children.len());
    let mut flowed_children: Vec<freecs::Entity> = Vec::with_capacity(children.len());
    let mut absolute_children: Vec<freecs::Entity> = Vec::new();
    for child in children {
        let Some(child_node) = world.ui.get_ui_layout_node(*child) else {
            continue;
        };
        if !child_node.visible {
            continue;
        }
        if child_node.base_layout.is_some() {
            absolute_children.push(*child);
            continue;
        }
        let child_node_id = build_taffy_subtree(world, *child, parent_size, dpi_scale);
        child_node_ids.push(child_node_id);
        flowed_children.push(*child);
    }
    world
        .resources
        .retained_ui
        .taffy
        .set_children(parent_entity, &child_node_ids);

    let resources = &mut world.resources;
    resources.retained_ui.taffy.compute_layout(
        parent_entity,
        available_size_for(parent_size),
        &mut resources.text.font_engine,
    );

    for child in &flowed_children {
        collect_region_rects(world, *child, parent_rect.min, dpi_scale, region_rects);
    }

    let mut rects = Vec::with_capacity(flowed_children.len() + absolute_children.len());
    for child in absolute_children {
        rects.push((child, None));
    }
    for child in flowed_children {
        rects.push((child, region_rects.get(&child).copied()));
    }
    rects
}

fn collect_region_rects(
    world: &World,
    entity: freecs::Entity,
    parent_abs_min: Vec2,
    dpi_scale: f32,
    region_rects: &mut std::collections::HashMap<freecs::Entity, Rect>,
) {
    let Some((location, size)) = world.resources.retained_ui.taffy.rect_for(entity) else {
        return;
    };
    let abs_min = parent_abs_min + location;
    region_rects.insert(entity, Rect::from_min_max(abs_min, abs_min + size));

    let Some(node) = world.ui.get_ui_layout_node(entity) else {
        return;
    };
    if node.flow_layout.is_none() && node.grid_layout.is_none() {
        return;
    }
    let child_origin = abs_min - node.scroll_offset * dpi_scale;

    if let Some(children) = world.resources.transform_state.children_cache.get(&entity) {
        for &child in children {
            let Some(child_node) = world.ui.get_ui_layout_node(child) else {
                continue;
            };
            if !child_node.visible {
                continue;
            }
            if child_node.base_layout.is_some() {
                continue;
            }
            collect_region_rects(world, child, child_origin, dpi_scale, region_rects);
        }
    }
}

fn read_region_children(
    world: &World,
    children: &[freecs::Entity],
    region_rects: &std::collections::HashMap<freecs::Entity, Rect>,
) -> Vec<(freecs::Entity, Option<Rect>)> {
    let mut rects = Vec::with_capacity(children.len());
    for child in children {
        let Some(child_node) = world.ui.get_ui_layout_node(*child) else {
            continue;
        };
        if !child_node.visible {
            continue;
        }
        if child_node.base_layout.is_some() {
            rects.push((*child, None));
        } else {
            rects.push((*child, region_rects.get(child).copied()));
        }
    }
    rects
}

fn build_taffy_subtree(
    world: &mut World,
    entity: freecs::Entity,
    parent_size: Vec2,
    dpi_scale: f32,
) -> taffy::NodeId {
    use crate::ecs::ui::taffy_layout::child_to_style;

    let (mut style, is_container, font_size) = match world.ui.get_ui_layout_node(entity) {
        Some(node) => (
            child_to_style(node, parent_size, dpi_scale),
            node.flow_layout.is_some() || node.grid_layout.is_some(),
            node.font_size.unwrap_or(16.0),
        ),
        None => (taffy::Style::default(), false, 16.0),
    };

    if is_container {
        apply_absolute_child_floor(&mut style, world, entity, parent_size, font_size, dpi_scale);
    }

    let node_id = world.resources.retained_ui.taffy.ensure_leaf(entity, style);

    if !is_container {
        let measured = measure_leaf_mut(world, entity, dpi_scale);
        world
            .resources
            .retained_ui
            .taffy
            .set_measured(entity, measured);
        update_wrap_text_cache(world, entity, dpi_scale);
        return node_id;
    }

    let child_count = world
        .resources
        .transform_state
        .children_cache
        .get(&entity)
        .map(|children| children.len())
        .unwrap_or(0);

    let entity_size = world
        .resources
        .retained_ui
        .taffy
        .rect_for(entity)
        .map(|(_, size)| size)
        .unwrap_or(parent_size);

    let mut child_node_ids: Vec<taffy::NodeId> = Vec::new();
    for index in 0..child_count {
        let Some(child) = world
            .resources
            .transform_state
            .children_cache
            .get(&entity)
            .and_then(|children| children.get(index).copied())
        else {
            continue;
        };
        let Some(child_node) = world.ui.get_ui_layout_node(child) else {
            continue;
        };
        if !child_node.visible {
            continue;
        }
        if child_node.base_layout.is_some() {
            continue;
        }
        let cnode = build_taffy_subtree(world, child, entity_size, dpi_scale);
        child_node_ids.push(cnode);
    }
    world
        .resources
        .retained_ui
        .taffy
        .set_children(entity, &child_node_ids);

    node_id
}

fn apply_absolute_child_floor(
    style: &mut taffy::Style,
    world: &World,
    container: freecs::Entity,
    parent_size: Vec2,
    font_size: f32,
    dpi_scale: f32,
) {
    use taffy::Dimension;

    let context = UiEvalContext {
        parent_width: 0.0,
        parent_height: 0.0,
        viewport_width: parent_size.x,
        viewport_height: parent_size.y,
        font_size,
        absolute_scale: dpi_scale,
    };
    let zero_parent = Rect::new(0.0, 0.0, 0.0, 0.0);

    let mut extent = Vec2::new(0.0, 0.0);
    if let Some(children) = world
        .resources
        .transform_state
        .children_cache
        .get(&container)
    {
        for &child in children {
            let Some(node) = world.ui.get_ui_layout_node(child) else {
                continue;
            };
            if !node.visible {
                continue;
            }
            let Some(base_layout) = node.base_layout.as_ref() else {
                continue;
            };
            let rect = compute_layout_rect(base_layout, &context, &zero_parent);
            extent.x = extent.x.max(rect.max.x);
            extent.y = extent.y.max(rect.max.y);
        }
    }

    if extent.x > 0.0 && matches!(style.size.width, Dimension::Auto) {
        style.min_size.width = dimension_floor(style.min_size.width, extent.x);
    }
    if extent.y > 0.0 && matches!(style.size.height, Dimension::Auto) {
        style.min_size.height = dimension_floor(style.min_size.height, extent.y);
    }
}

fn dimension_floor(dimension: taffy::Dimension, floor: f32) -> taffy::Dimension {
    use taffy::prelude::FromLength;

    match dimension {
        taffy::Dimension::Length(value) => taffy::Dimension::from_length(value.max(floor)),
        _ => taffy::Dimension::from_length(floor),
    }
}

fn update_wrap_text_cache(world: &mut World, entity: freecs::Entity, dpi_scale: f32) {
    use crate::ecs::ui::components::{TextOverflow, UiNodeContent};

    let info = (|| {
        let content = world.ui.get_ui_node_content(entity)?;
        let layout_node = world.ui.get_ui_layout_node(entity)?;
        let UiNodeContent::Text {
            text_slot,
            font_size_override,
            overflow,
            ..
        } = content
        else {
            return None;
        };
        if !matches!(overflow, TextOverflow::Wrap) {
            return None;
        }
        let base_font = layout_node.font_size.unwrap_or(16.0);
        let font_size = font_size_override.unwrap_or(base_font) * dpi_scale;
        let text = world.resources.text.cache.get_text(*text_slot)?.to_string();
        Some(crate::ecs::ui::taffy_layout::WrapTextMeasure { text, font_size })
    })();

    match info {
        Some(info) => world
            .resources
            .retained_ui
            .taffy
            .set_wrap_text(entity, info),
        None => world.resources.retained_ui.taffy.clear_wrap_text(entity),
    }
}

fn measure_leaf_mut(world: &mut World, entity: freecs::Entity, dpi_scale: f32) -> taffy::Size<f32> {
    use crate::ecs::ui::components::UiNodeContent;

    let (text_slot, font_size_override, base_font) = {
        let Some(content) = world.ui.get_ui_node_content(entity) else {
            return taffy::Size::ZERO;
        };
        let Some(node) = world.ui.get_ui_layout_node(entity) else {
            return taffy::Size::ZERO;
        };
        let UiNodeContent::Text {
            text_slot,
            font_size_override,
            ..
        } = content
        else {
            return taffy::Size::ZERO;
        };
        (
            *text_slot,
            *font_size_override,
            node.font_size.unwrap_or(16.0),
        )
    };
    let font_size = font_size_override.unwrap_or(base_font) * dpi_scale;
    let text = match world.resources.text.cache.get_text(text_slot) {
        Some(text) => text.to_string(),
        None => return taffy::Size::ZERO,
    };
    let width = crate::ecs::ui::widget_systems::measure_text_width(
        &mut world.resources.text.font_engine,
        &text,
        font_size,
    );
    let line_height = font_size * 1.2;
    taffy::Size {
        width,
        height: line_height,
    }
}

fn prune_taffy(world: &mut World) {
    let mut live: std::collections::HashSet<freecs::Entity> = std::collections::HashSet::new();
    let mut stack: Vec<freecs::Entity> = world
        .ui
        .query_entities(crate::ecs::world::UI_LAYOUT_ROOT)
        .collect();
    while let Some(entity) = stack.pop() {
        if !live.insert(entity) {
            continue;
        }
        if let Some(children) = world.resources.transform_state.children_cache.get(&entity) {
            stack.extend(children.iter().copied());
        }
    }
    let stale: Vec<freecs::Entity> = world
        .resources
        .retained_ui
        .taffy
        .entity_to_node
        .keys()
        .copied()
        .filter(|entity| !live.contains(entity))
        .collect();
    for entity in stale {
        world.resources.retained_ui.taffy.release(entity);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ecs::transform::systems::validate_and_rebuild_children_cache;
    use crate::ecs::ui::builder::UiTreeBuilder;
    use crate::ecs::ui::layout_types::FlowDirection;
    use crate::ecs::ui::types::Anchor;
    use crate::ecs::ui::units::{Ab, Rl};

    fn setup_world(width: u32, height: u32) -> World {
        let mut world = World::default();
        world.resources.window.cached_viewport_size = Some((width, height));
        world.resources.window.cached_scale_factor = 1.0;
        world.resources.retained_ui.enabled = true;
        world
    }

    fn compute_layout(world: &mut World) {
        validate_and_rebuild_children_cache(world);
        world.resources.retained_ui.dirty.layout_dirty = true;
        ui_layout_compute_system(world);
    }

    fn rect_of(world: &World, entity: freecs::Entity) -> Rect {
        world
            .ui
            .get_ui_layout_node(entity)
            .expect("layout node present")
            .computed_rect
    }

    fn assert_rect(actual: Rect, min: Vec2, max: Vec2) {
        let epsilon = 0.05;
        let close = (actual.min.x - min.x).abs() < epsilon
            && (actual.min.y - min.y).abs() < epsilon
            && (actual.max.x - max.x).abs() < epsilon
            && (actual.max.y - max.y).abs() < epsilon;
        assert!(
            close,
            "expected min {:?} max {:?}, got min {:?} max {:?}",
            min, max, actual.min, actual.max
        );
    }

    #[test]
    fn vertical_flow_stacks_children_with_gap() {
        let mut world = setup_world(1000, 800);
        let container;
        let first;
        let second;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 10.0)
                .entity();
            (first, second) = tree.in_parent(container, |tree| {
                let first = tree
                    .add_node()
                    .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
                    .entity();
                let second = tree
                    .add_node()
                    .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
                    .entity();
                (first, second)
            });
            tree.finish();
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, container),
            Vec2::new(0.0, 0.0),
            Vec2::new(1000.0, 800.0),
        );
        assert_rect(
            rect_of(&world, first),
            Vec2::new(0.0, 0.0),
            Vec2::new(1000.0, 100.0),
        );
        assert_rect(
            rect_of(&world, second),
            Vec2::new(0.0, 110.0),
            Vec2::new(1000.0, 210.0),
        );
    }

    #[test]
    fn absolute_scale_shrinks_flow_children_and_gap() {
        let mut world = setup_world(1000, 800);
        let left;
        let right;
        {
            let mut tree = UiTreeBuilder::new(&mut world).with_absolute_scale(0.5);
            let container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Horizontal, 0.0, 20.0)
                .entity();
            (left, right) = tree.in_parent(container, |tree| {
                let left = tree
                    .add_node()
                    .flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
                    .entity();
                let right = tree
                    .add_node()
                    .flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
                    .entity();
                (left, right)
            });
            tree.finish();
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, left),
            Vec2::new(0.0, 0.0),
            Vec2::new(100.0, 800.0),
        );
        assert_rect(
            rect_of(&world, right),
            Vec2::new(110.0, 0.0),
            Vec2::new(210.0, 800.0),
        );
    }

    #[test]
    fn nested_flow_region_resolves_grandchildren() {
        let mut world = setup_world(1000, 800);
        let outer;
        let inner;
        let left;
        let right;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            outer = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            (inner, left, right) = tree.in_parent(outer, |tree| {
                let inner = tree
                    .add_node()
                    .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 200.0)))
                    .flow(FlowDirection::Horizontal, 0.0, 20.0)
                    .entity();
                let (left, right) = tree.in_parent(inner, |tree| {
                    let left = tree
                        .add_node()
                        .flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
                        .entity();
                    let right = tree
                        .add_node()
                        .flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
                        .entity();
                    (left, right)
                });
                (inner, left, right)
            });
            tree.finish();
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, inner),
            Vec2::new(0.0, 0.0),
            Vec2::new(1000.0, 200.0),
        );
        assert_rect(
            rect_of(&world, left),
            Vec2::new(0.0, 0.0),
            Vec2::new(200.0, 200.0),
        );
        assert_rect(
            rect_of(&world, right),
            Vec2::new(220.0, 0.0),
            Vec2::new(420.0, 200.0),
        );
    }

    #[test]
    fn scroll_offset_shifts_flow_children() {
        let mut world = setup_world(1000, 800);
        let container;
        let first;
        let second;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 10.0)
                .entity();
            (first, second) = tree.in_parent(container, |tree| {
                let first = tree
                    .add_node()
                    .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
                    .entity();
                let second = tree
                    .add_node()
                    .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
                    .entity();
                (first, second)
            });
            tree.finish();
        }

        if let Some(node) = world.ui.get_ui_layout_node_mut(container) {
            node.scroll_offset = Vec2::new(0.0, 50.0);
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, first),
            Vec2::new(0.0, -50.0),
            Vec2::new(1000.0, 50.0),
        );
        assert_rect(
            rect_of(&world, second),
            Vec2::new(0.0, 60.0),
            Vec2::new(1000.0, 160.0),
        );
    }

    #[test]
    fn auto_container_grows_to_fit_absolute_child() {
        let mut world = setup_world(1000, 800);
        let inner;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            let outer = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            inner = tree.in_parent(outer, |tree| {
                let inner = tree
                    .add_node()
                    .flow(FlowDirection::Vertical, 0.0, 0.0)
                    .entity();
                tree.in_parent(inner, |tree| {
                    tree.add_node()
                        .window(
                            Ab(Vec2::new(0.0, 0.0)),
                            Ab(Vec2::new(300.0, 150.0)),
                            Anchor::TopLeft,
                        )
                        .entity();
                });
                inner
            });
            tree.finish();
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, inner),
            Vec2::new(0.0, 0.0),
            Vec2::new(300.0, 150.0),
        );
    }

    #[test]
    fn property_grid_rows_stack_without_overlap() {
        let mut world = setup_world(1000, 800);
        let grid;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            let container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            grid = tree.in_parent(container, |tree| {
                let grid = tree.add_property_grid(96.0);
                let section = tree.add_property_section(grid, "Transform");
                tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
                tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
                tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
                grid
            });
            tree.finish();
        }

        compute_layout(&mut world);

        let rows = world
            .ui
            .get_ui_property_grid(grid)
            .expect("property grid data")
            .row_entities
            .clone();
        assert_eq!(rows.len(), 3, "three drag-value rows expected");

        let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
        for rect in &rects {
            let height = rect.height();
            assert!(
                height > 1.0 && height < 80.0,
                "a property row should be a single line tall, got {height}"
            );
        }

        rects.sort_by(|a, b| {
            a.min
                .y
                .partial_cmp(&b.min.y)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        for pair in rects.windows(2) {
            assert!(
                pair[1].min.y >= pair[0].max.y - 0.5,
                "rows overlap: {:?} then {:?}",
                pair[0],
                pair[1]
            );
        }
    }

    #[test]
    fn docked_panel_scroll_grid_diagnostic() {
        let mut world = setup_world(1920, 1080);
        world.resources.schedules.retained_ui =
            crate::schedule::build_default_retained_ui_schedule();
        let grid;
        let panel;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            panel = tree.add_docked_panel_right("inspector", "Inspector", 320.0);
            let content = tree
                .world_mut()
                .ui
                .get_ui_panel(panel)
                .map(|data| data.content_entity)
                .expect("panel content");
            grid = tree.in_parent(content, |tree| {
                let scroll = tree.add_scroll_area_fill(0.0, 0.0);
                let body = tree
                    .world_mut()
                    .ui
                    .get_ui_scroll_area(scroll)
                    .map(|data| data.content_entity)
                    .unwrap_or(scroll);
                tree.in_parent(body, |tree| {
                    let grid = tree.add_property_grid(96.0);
                    let section = tree.add_property_section(grid, "Transform");
                    tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
                    tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
                    tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
                    grid
                })
            });
            tree.finish();
        }

        for _ in 0..2 {
            validate_and_rebuild_children_cache(&mut world);
            crate::schedule::run_retained_ui_schedule(&mut world);
        }

        let panel_rect = rect_of(&world, panel);
        assert!(
            (panel_rect.min.x - 1600.0).abs() < 1.0,
            "right-docked panel should start at viewport width minus its size, got {}",
            panel_rect.min.x
        );

        let rows = world
            .ui
            .get_ui_property_grid(grid)
            .expect("property grid data")
            .row_entities
            .clone();
        let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
        for rect in &rects {
            assert!(
                rect.height() > 1.0 && rect.height() < 80.0,
                "row inside the docked panel should be one line tall, got {}",
                rect.height()
            );
            assert!(
                rect.min.x >= panel_rect.min.x && rect.max.x <= panel_rect.max.x + 0.5,
                "row should sit within the panel bounds"
            );
        }
        rects.sort_by(|a, b| {
            a.min
                .y
                .partial_cmp(&b.min.y)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        for pair in rects.windows(2) {
            assert!(
                pair[1].min.y >= pair[0].max.y - 0.5,
                "rows overlap inside the docked panel: {:?} then {:?}",
                pair[0],
                pair[1]
            );
        }
    }

    #[test]
    fn property_grid_rows_stack_at_high_dpi() {
        let mut world = setup_world(1280, 1600);
        world.resources.window.cached_scale_factor = 2.0;
        let grid;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            let container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            grid = tree.in_parent(container, |tree| {
                let grid = tree.add_property_grid(96.0);
                let section = tree.add_property_section(grid, "Transform");
                tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
                tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
                tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
                grid
            });
            tree.finish();
        }

        compute_layout(&mut world);

        let rows = world
            .ui
            .get_ui_property_grid(grid)
            .expect("property grid data")
            .row_entities
            .clone();
        let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
        for rect in &rects {
            let height = rect.height();
            assert!(
                height > 1.0 && height < 160.0,
                "at dpi 2 a property row should be one scaled line, got {height}"
            );
        }
        rects.sort_by(|a, b| {
            a.min
                .y
                .partial_cmp(&b.min.y)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        for pair in rects.windows(2) {
            assert!(
                pair[1].min.y >= pair[0].max.y - 0.5,
                "rows overlap at dpi 2: {:?} then {:?}",
                pair[0],
                pair[1]
            );
        }
    }

    #[test]
    fn property_grid_mixed_widgets_stack_without_overlap() {
        let mut world = setup_world(1000, 800);
        let grid;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            let container = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            grid = tree.in_parent(container, |tree| {
                let grid = tree.add_property_grid(96.0);
                let section = tree.add_property_section(grid, "Mixed");
                tree.add_property_drag_value(grid, section, "Drag", -100.0, 100.0, 1.0);
                tree.add_property_checkbox(grid, section, "Check", true);
                tree.add_property_dropdown(grid, section, "Pick", &["A", "B", "C"], 0);
                tree.add_property_text_input(grid, section, "Text", "value");
                grid
            });
            tree.finish();
        }

        compute_layout(&mut world);

        let rows = world
            .ui
            .get_ui_property_grid(grid)
            .expect("property grid data")
            .row_entities
            .clone();
        assert_eq!(rows.len(), 4, "four widget rows expected");

        let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
        for rect in &rects {
            let height = rect.height();
            assert!(
                height > 1.0 && height < 80.0,
                "a property row should be a single line tall, got {height}"
            );
            assert!(
                rect.width() > 200.0,
                "a property row should span the grid width, got {}",
                rect.width()
            );
        }

        rects.sort_by(|a, b| {
            a.min
                .y
                .partial_cmp(&b.min.y)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        for pair in rects.windows(2) {
            assert!(
                pair[1].min.y >= pair[0].max.y - 0.5,
                "rows overlap: {:?} then {:?}",
                pair[0],
                pair[1]
            );
        }
    }

    #[test]
    fn explicit_size_container_ignores_absolute_child() {
        let mut world = setup_world(1000, 800);
        let inner;
        {
            let mut tree = UiTreeBuilder::new(&mut world);
            let outer = tree
                .add_node()
                .boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                .flow(FlowDirection::Vertical, 0.0, 0.0)
                .entity();
            inner = tree.in_parent(outer, |tree| {
                let inner = tree
                    .add_node()
                    .flow_child(Ab(Vec2::new(120.0, 40.0)))
                    .flow(FlowDirection::Vertical, 0.0, 0.0)
                    .entity();
                tree.in_parent(inner, |tree| {
                    tree.add_node()
                        .window(
                            Ab(Vec2::new(0.0, 0.0)),
                            Ab(Vec2::new(300.0, 150.0)),
                            Anchor::TopLeft,
                        )
                        .entity();
                });
                inner
            });
            tree.finish();
        }

        compute_layout(&mut world);

        assert_rect(
            rect_of(&world, inner),
            Vec2::new(0.0, 0.0),
            Vec2::new(120.0, 40.0),
        );
    }
}
