//! Brush element edit modes and viewport element picking.

use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use shared::{
    brush_math::{validate_brush, BrushDiagnosticSeverity},
    BrushDesc, BrushFaceDesc, BrushPlaneDesc, ComponentInstanceId,
};

use crate::camera::EditorCamera;
use crate::history::{push_command, set_brush_transform_with_history, EditorCommand};
use crate::infra::EditorOnly;
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
use crate::scene_io::SceneIo;
use crate::selection::ViewportClick;
use crate::state::scene_tools_active;
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
use crate::viewport::{scene_view_ray, ViewportDisplayMode};
use transform_gizmo_bevy::prelude::GizmoTarget;

#[derive(Resource, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BrushEditMode {
    #[default]
    Object,
    Vertex,
    Edge,
    Face,
    Clip,
}

impl BrushEditMode {
    pub fn label(self) -> &'static str {
        match self {
            Self::Object => "Object",
            Self::Vertex => "Vertex",
            Self::Edge => "Edge",
            Self::Face => "Face",
            Self::Clip => "Clip",
        }
    }

    pub fn is_element_mode(self) -> bool {
        self != Self::Object
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum BrushElementKey {
    Vertex {
        face: ComponentInstanceId,
        index: usize,
    },
    Edge {
        face: ComponentInstanceId,
        index: usize,
    },
    Face {
        face: ComponentInstanceId,
    },
}

#[derive(Resource, Default, Debug, Clone)]
pub struct BrushElementSelection {
    pub brush: Option<Entity>,
    pub elements: Vec<BrushElementKey>,
}

impl BrushElementSelection {
    fn replace(&mut self, brush: Entity, element: BrushElementKey) {
        self.brush = Some(brush);
        self.elements.clear();
        self.elements.push(element);
    }

    fn toggle(&mut self, brush: Entity, element: BrushElementKey) {
        if self.brush != Some(brush) {
            self.replace(brush, element);
            return;
        }
        if let Some(index) = self
            .elements
            .iter()
            .position(|existing| existing == &element)
        {
            self.elements.remove(index);
        } else {
            self.elements.push(element);
        }
    }

    pub fn clear(&mut self) {
        self.brush = None;
        self.elements.clear();
    }
}

pub struct BrushEditPlugin;

impl Plugin for BrushEditPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<BrushEditMode>()
            .init_resource::<BrushElementSelection>()
            .init_resource::<BrushElementGizmoState>()
            .add_systems(
                Update,
                (
                    brush_edit_hotkeys,
                    brush_element_pick,
                    sync_brush_element_gizmo,
                    brush_clip_commit,
                    draw_brush_edit_overlays,
                )
                    .chain()
                    .run_if(scene_tools_active),
            )
            .add_systems(Last, apply_brush_element_gizmo.run_if(scene_tools_active));
    }
}

#[derive(Component, Debug, Clone, Copy)]
pub struct BrushElementGizmo;

#[derive(Resource, Default, Debug, Clone)]
struct BrushElementGizmoState {
    entity: Option<Entity>,
    brush: Option<Entity>,
    old_brush: Option<BrushDesc>,
    old_gizmo: Option<Transform>,
    last_gizmo: Option<Transform>,
    changed: bool,
}

#[allow(clippy::too_many_arguments)]
fn brush_edit_hotkeys(
    mut mode: ResMut<BrushEditMode>,
    mut element_selection: ResMut<BrushElementSelection>,
    mut active_operator: ResMut<ActiveOperator>,
    mut scene_io: ResMut<SceneIo>,
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    display: Res<ViewportDisplayMode>,
    brushes: Query<(), With<BrushDesc>>,
) -> Result {
    if display.clean_game_view {
        return Ok(());
    }
    let ctx = contexts.ctx_mut()?;
    if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons)
        || ctx.egui_wants_keyboard_input()
    {
        return Ok(());
    }
    if keys.just_pressed(KeyCode::Escape) && mode.is_element_mode() {
        *mode = BrushEditMode::Object;
        element_selection.clear();
        set_brush_edit_status(&mut active_operator, OperatorPhase::Canceled, "Object mode");
        scene_io.status = "Brush edit: Object mode".to_string();
        return Ok(());
    }

    let requested = if keys.just_pressed(KeyCode::Digit1) {
        Some(BrushEditMode::Vertex)
    } else if keys.just_pressed(KeyCode::Digit2) {
        Some(BrushEditMode::Edge)
    } else if keys.just_pressed(KeyCode::Digit3) {
        Some(BrushEditMode::Face)
    } else if keys.just_pressed(KeyCode::Digit4) {
        Some(BrushEditMode::Clip)
    } else {
        None
    };
    let Some(requested) = requested else {
        return Ok(());
    };
    let Some(selected_brush) = selected_brush_entity(&ui_state, &brushes) else {
        scene_io.status = "Select a brush before entering brush edit mode".to_string();
        set_brush_edit_status(
            &mut active_operator,
            OperatorPhase::Blocked,
            "Select a brush before editing elements",
        );
        return Ok(());
    };

    *mode = requested;
    element_selection.brush = Some(selected_brush);
    element_selection.elements.clear();
    scene_io.status = format!("Brush edit: {} mode", mode.label());
    set_brush_edit_status(
        &mut active_operator,
        OperatorPhase::Preview,
        format!("{} mode", mode.label()),
    );
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn brush_element_pick(
    mode: Res<BrushEditMode>,
    mut viewport_click: ResMut<ViewportClick>,
    mut element_selection: ResMut<BrushElementSelection>,
    keys: Res<ButtonInput<KeyCode>>,
    ui_state: Res<UiState>,
    cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    brushes: Query<(Entity, &BrushDesc, &GlobalTransform)>,
) {
    if !mode.is_element_mode() {
        return;
    }
    let Some(pointer_pos) = viewport_click.0.take() else {
        return;
    };
    let Some(ray) = viewport_ray(&cameras, pointer_pos, ui_state.viewport_rect) else {
        return;
    };
    let brush_filter = element_selection.brush;
    let mut best: Option<(Entity, BrushElementKey, f32)> = None;
    for (entity, brush, transform) in &brushes {
        if brush_filter.is_some_and(|selected| selected != entity) {
            continue;
        }
        if let Some((element, distance)) = pick_element(*mode, brush, transform, ray) {
            if best
                .as_ref()
                .is_none_or(|(_, _, best_distance)| distance < *best_distance)
            {
                best = Some((entity, element, distance));
            }
        }
    }
    let Some((brush, element, _)) = best else {
        if !keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
            element_selection.clear();
        }
        return;
    };
    if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
        element_selection.toggle(brush, element);
    } else {
        element_selection.replace(brush, element);
    }
}

fn sync_brush_element_gizmo(
    mut commands: Commands,
    mode: Res<BrushEditMode>,
    selection: Res<BrushElementSelection>,
    mut state: ResMut<BrushElementGizmoState>,
    mut brushes: Query<(&mut BrushDesc, &GlobalTransform)>,
    mut gizmos: Query<&mut Transform, With<BrushElementGizmo>>,
) {
    if !mode.is_element_mode() || matches!(*mode, BrushEditMode::Clip) {
        if let Some((brush_entity, old_brush)) = take_brush_edit_rollback(&mut state) {
            if let Ok((mut brush, _)) = brushes.get_mut(brush_entity) {
                *brush = old_brush;
            }
        }
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    }
    let Some(brush_entity) = selection.brush else {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    };
    if selection.elements.is_empty() {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    }
    let Ok((brush, brush_transform)) = brushes.get(brush_entity) else {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    };
    let Some(pivot) = selected_element_world_pivot(brush, brush_transform, &selection.elements)
    else {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    };

    let rotation = brush_transform.to_scale_rotation_translation().1;
    let desired = Transform::from_translation(pivot).with_rotation(rotation);
    let needs_new_entity = state.brush != Some(brush_entity)
        || state
            .entity
            .is_none_or(|entity| gizmos.get(entity).is_err());
    if needs_new_entity {
        clear_brush_element_gizmo(&mut commands, &mut state);
        let entity = commands
            .spawn((
                BrushElementGizmo,
                EditorOnly,
                GizmoTarget::default(),
                desired,
                GlobalTransform::default(),
                Visibility::Hidden,
                InheritedVisibility::default(),
            ))
            .id();
        state.entity = Some(entity);
        state.brush = Some(brush_entity);
        state.old_brush = None;
        state.old_gizmo = Some(desired);
        state.last_gizmo = Some(desired);
        state.changed = false;
        return;
    }

    let Some(entity) = state.entity else {
        return;
    };
    let Ok(mut transform) = gizmos.get_mut(entity) else {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    };
    if state.old_brush.is_none() {
        let current = *transform;
        if !gizmo_transform_is_active(&state, current) {
            *transform = desired;
            state.old_gizmo = Some(desired);
            state.last_gizmo = Some(desired);
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn apply_brush_element_gizmo(
    mut commands: Commands,
    mode: Res<BrushEditMode>,
    selection: Res<BrushElementSelection>,
    mut state: ResMut<BrushElementGizmoState>,
    mut active_operator: ResMut<ActiveOperator>,
    mut brushes: Query<(&mut BrushDesc, &GlobalTransform)>,
    gizmos: Query<(&Transform, &GizmoTarget), With<BrushElementGizmo>>,
) {
    if !mode.is_element_mode() || matches!(*mode, BrushEditMode::Clip) {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    }
    let (Some(gizmo_entity), Some(brush_entity)) = (state.entity, selection.brush) else {
        return;
    };
    if selection.elements.is_empty() || state.brush != Some(brush_entity) {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    }
    let Ok((gizmo_transform, target)) = gizmos.get(gizmo_entity) else {
        clear_brush_element_gizmo(&mut commands, &mut state);
        return;
    };

    if target.is_active() {
        let Ok((mut brush, brush_transform)) = brushes.get_mut(brush_entity) else {
            clear_brush_element_gizmo(&mut commands, &mut state);
            return;
        };
        if state.old_brush.is_none() {
            state.old_brush = Some(brush.clone());
            state.old_gizmo = Some(*gizmo_transform);
            state.last_gizmo = Some(*gizmo_transform);
            state.changed = false;
            return;
        }
        let Some(last_gizmo) = state.last_gizmo else {
            state.last_gizmo = Some(*gizmo_transform);
            return;
        };
        if !transforms_nearly_equal(last_gizmo, *gizmo_transform)
            && transform_selected_elements_by_gizmo(
                &mut brush,
                &selection.elements,
                brush_transform,
                last_gizmo,
                *gizmo_transform,
            )
        {
            state.changed = true;
            state.last_gizmo = Some(*gizmo_transform);
            set_brush_edit_status(
                &mut active_operator,
                OperatorPhase::Preview,
                format!(
                    "Gizmo editing {} brush element(s)",
                    selection.elements.len()
                ),
            );
        }
        return;
    }

    if let (Some(old), Some(_)) = (state.old_brush.take(), state.old_gizmo.take()) {
        if state.changed {
            let Ok((mut brush, _)) = brushes.get_mut(brush_entity) else {
                clear_brush_element_gizmo(&mut commands, &mut state);
                return;
            };
            let new = brush.clone();
            let validation = validate_brush(&new);
            if validation.is_valid() {
                commands.queue(move |world: &mut World| {
                    push_command(
                        world,
                        EditorCommand::SetBrush {
                            entity: brush_entity,
                            old: Some(old),
                            new,
                        },
                    );
                });
                set_brush_edit_status(
                    &mut active_operator,
                    OperatorPhase::Committed,
                    "Committed brush element gizmo edit",
                );
            } else {
                *brush = old;
                let message = validation
                    .diagnostics
                    .iter()
                    .find(|diagnostic| diagnostic.severity == BrushDiagnosticSeverity::Error)
                    .map(|diagnostic| diagnostic.message.as_str())
                    .unwrap_or("Brush edit produced invalid geometry");
                set_brush_edit_status(
                    &mut active_operator,
                    OperatorPhase::Blocked,
                    format!("Rejected brush edit: {message}"),
                );
            }
        }
    }
    state.last_gizmo = Some(*gizmo_transform);
    state.changed = false;
}

fn take_brush_edit_rollback(state: &mut BrushElementGizmoState) -> Option<(Entity, BrushDesc)> {
    let rollback = if state.changed {
        state.brush.zip(state.old_brush.take())
    } else {
        None
    };
    state.old_brush = None;
    state.old_gizmo = None;
    state.last_gizmo = None;
    state.changed = false;
    rollback
}

fn clear_brush_element_gizmo(commands: &mut Commands, state: &mut BrushElementGizmoState) {
    if let Some(entity) = state.entity.take() {
        if let Ok(mut entity_mut) = commands.get_entity(entity) {
            entity_mut.despawn();
        }
    }
    state.brush = None;
    state.old_brush = None;
    state.old_gizmo = None;
    state.last_gizmo = None;
    state.changed = false;
}

fn selected_element_world_pivot(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    selected: &[BrushElementKey],
) -> Option<Vec3> {
    let anchors = selected_anchor_positions(brush, selected);
    if anchors.is_empty() {
        return None;
    }
    let affine = transform.affine();
    Some(
        anchors
            .iter()
            .map(|anchor| affine.transform_point3(*anchor))
            .sum::<Vec3>()
            / anchors.len() as f32,
    )
}

fn gizmo_transform_is_active(state: &BrushElementGizmoState, transform: Transform) -> bool {
    state
        .last_gizmo
        .is_some_and(|last| !transforms_nearly_equal(last, transform))
        || state.old_brush.is_some()
}

fn transform_selected_elements_by_gizmo(
    brush: &mut BrushDesc,
    selected: &[BrushElementKey],
    brush_transform: &GlobalTransform,
    old_gizmo: Transform,
    new_gizmo: Transform,
) -> bool {
    let anchors = selected_anchor_positions(brush, selected);
    if anchors.is_empty() {
        return false;
    }
    let old_world_from_gizmo = old_gizmo.compute_affine();
    let new_world_from_gizmo = new_gizmo.compute_affine();
    let world_delta = new_world_from_gizmo * old_world_from_gizmo.inverse();
    let brush_from_world = brush_transform.affine().inverse();
    let world_from_brush = brush_transform.affine();
    let mut changed = false;
    for face in &mut brush.faces {
        for vertex in &mut face.vertices {
            if anchors
                .iter()
                .any(|anchor| vertex.distance_squared(*anchor) <= 0.0001)
            {
                let world = world_from_brush.transform_point3(*vertex);
                let transformed = world_delta.transform_point3(world);
                let local = brush_from_world.transform_point3(transformed);
                if vertex.distance_squared(local) > 0.000001 {
                    *vertex = local;
                    changed = true;
                }
            }
        }
    }
    if changed {
        recompute_face_planes(brush);
    }
    changed
}

fn transforms_nearly_equal(a: Transform, b: Transform) -> bool {
    a.translation.distance_squared(b.translation) <= 0.000001
        && a.rotation.dot(b.rotation).abs() >= 0.99999
        && a.scale.distance_squared(b.scale) <= 0.000001
}

fn brush_clip_commit(
    mode: Res<BrushEditMode>,
    selection: Res<BrushElementSelection>,
    keys: Res<ButtonInput<KeyCode>>,
    mut scene_io: ResMut<SceneIo>,
    mut commands: Commands,
    brushes: Query<(&BrushDesc, &Transform)>,
) {
    if !matches!(*mode, BrushEditMode::Clip) || !keys.just_pressed(KeyCode::Enter) {
        return;
    }
    let Some(brush_entity) = selection.brush else {
        scene_io.status = "Clip failed: select a brush face".into();
        return;
    };
    let Some(face_id) = single_selected_face(&selection) else {
        scene_io.status = "Clip failed: select exactly one brush face".into();
        return;
    };
    let Ok((brush, transform)) = brushes.get(brush_entity) else {
        scene_io.status = "Clip failed: selected brush no longer exists".into();
        return;
    };
    let Some(result) = clip_preview_result(brush, transform, face_id) else {
        scene_io.status = "Clip failed: selected face cannot define a clip plane".into();
        return;
    };
    scene_io.status = "Brush clip committed".into();
    commands.queue(move |world: &mut World| {
        set_brush_transform_with_history(world, brush_entity, result.transform, result.brush);
    });
}

fn draw_brush_edit_overlays(
    mode: Res<BrushEditMode>,
    selection: Res<BrushElementSelection>,
    ui_state: Res<UiState>,
    brushes: Query<(Entity, &BrushDesc, &GlobalTransform)>,
    mut gizmos: Gizmos,
) {
    if !mode.is_element_mode() {
        return;
    }
    let selected_brushes: Vec<Entity> = ui_state.selected_entities.iter().collect();
    for (entity, brush, transform) in &brushes {
        if !selected_brushes.contains(&entity) {
            continue;
        }
        let active = selection.brush == Some(entity);
        draw_brush_elements(
            *mode,
            brush,
            transform,
            &selection.elements,
            active,
            &mut gizmos,
        );
        if matches!(*mode, BrushEditMode::Clip) && active {
            if let Some(face_id) = single_selected_face(&selection) {
                if let Some(result) = clip_preview_result_from_global(brush, transform, face_id) {
                    draw_clip_preview(&mut gizmos, result.world_min, result.world_max);
                }
            }
        }
    }
}

fn draw_brush_elements(
    mode: BrushEditMode,
    brush: &BrushDesc,
    transform: &GlobalTransform,
    selected: &[BrushElementKey],
    active: bool,
    gizmos: &mut Gizmos,
) {
    let base_color = match mode {
        BrushEditMode::Vertex => Color::srgb(0.31, 0.75, 0.88),
        BrushEditMode::Edge => Color::srgb(0.95, 0.62, 0.2),
        BrushEditMode::Face => Color::srgb(0.32, 0.82, 0.52),
        BrushEditMode::Clip => Color::srgb(0.86, 0.42, 0.92),
        BrushEditMode::Object => Color::WHITE,
    }
    .with_alpha(if active { 0.9 } else { 0.38 });
    let selected_color = Color::srgb(1.0, 0.76, 0.34);
    for face in &brush.faces {
        let vertices = world_vertices(face, transform);
        if vertices.is_empty() {
            continue;
        }
        let face_center = average_point(&vertices);
        let normal = transform
            .affine()
            .transform_vector3(face.plane.normal)
            .normalize_or_zero();
        if matches!(mode, BrushEditMode::Face | BrushEditMode::Clip) {
            let key = BrushElementKey::Face {
                face: face.id.clone(),
            };
            let color = if active && selected.contains(&key) {
                selected_color
            } else {
                base_color
            };
            gizmos.sphere(
                face_center,
                if active && selected.contains(&key) {
                    0.11
                } else {
                    0.075
                },
                color,
            );
            gizmos.line(face_center, face_center + normal * 0.65, color);
            if matches!(mode, BrushEditMode::Face) {
                for index in 0..vertices.len() {
                    gizmos.line(
                        vertices[index],
                        vertices[(index + 1) % vertices.len()],
                        color.with_alpha(if active && selected.contains(&key) {
                            1.0
                        } else {
                            0.42
                        }),
                    );
                }
            }
        }
        for index in 0..vertices.len() {
            let next = (index + 1) % vertices.len();
            if matches!(mode, BrushEditMode::Vertex) {
                let key = BrushElementKey::Vertex {
                    face: face.id.clone(),
                    index,
                };
                let color = if active && selected.contains(&key) {
                    selected_color
                } else {
                    base_color
                };
                gizmos.sphere(
                    vertices[index],
                    if active && selected.contains(&key) {
                        0.1
                    } else {
                        0.055
                    },
                    color,
                );
            }
            if matches!(mode, BrushEditMode::Edge | BrushEditMode::Clip) {
                let key = BrushElementKey::Edge {
                    face: face.id.clone(),
                    index,
                };
                let color = if active && selected.contains(&key) {
                    selected_color
                } else {
                    base_color
                };
                gizmos.line(vertices[index], vertices[next], color);
                if active && selected.contains(&key) {
                    gizmos.sphere(vertices[index], 0.07, color);
                    gizmos.sphere(vertices[next], 0.07, color);
                }
            }
        }
    }
}

#[derive(Debug, Clone)]
struct ClipPreviewResult {
    brush: BrushDesc,
    transform: Transform,
    world_min: Vec3,
    world_max: Vec3,
}

fn single_selected_face(selection: &BrushElementSelection) -> Option<&ComponentInstanceId> {
    let mut faces = selection
        .elements
        .iter()
        .filter_map(|element| match element {
            BrushElementKey::Face { face } => Some(face),
            _ => None,
        });
    let face = faces.next()?;
    faces.next().is_none().then_some(face)
}

fn clip_preview_result(
    brush: &BrushDesc,
    transform: &Transform,
    face_id: &ComponentInstanceId,
) -> Option<ClipPreviewResult> {
    let bounds = local_brush_bounds(brush)?;
    let face = brush.faces.iter().find(|face| &face.id == face_id)?;
    let normal = face.plane.normal.try_normalize()?;
    let (axis, sign) = dominant_axis(normal)?;
    let mut min = bounds.0;
    let mut max = bounds.1;
    let center = (min + max) * 0.5;
    match (axis, sign.is_sign_positive()) {
        (0, true) => min.x = center.x,
        (0, false) => max.x = center.x,
        (1, true) => min.y = center.y,
        (1, false) => max.y = center.y,
        (2, true) => min.z = center.z,
        (2, false) => max.z = center.z,
        _ => return None,
    }
    if (max - min).cmple(Vec3::splat(0.001)).any() {
        return None;
    }
    let local_center = (min + max) * 0.5;
    let local_size = max - min;
    let mut new_transform = *transform;
    new_transform.translation = transform.compute_affine().transform_point3(local_center);
    let brush = BrushDesc::cuboid(local_size);
    let (world_min, world_max) = world_bounds_for_local_box(min, max, transform);
    Some(ClipPreviewResult {
        brush,
        transform: new_transform,
        world_min,
        world_max,
    })
}

fn clip_preview_result_from_global(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    face_id: &ComponentInstanceId,
) -> Option<ClipPreviewResult> {
    let (scale, rotation, translation) = transform.to_scale_rotation_translation();
    let local_transform = Transform {
        translation,
        rotation,
        scale,
    };
    clip_preview_result(brush, &local_transform, face_id)
}

fn local_brush_bounds(brush: &BrushDesc) -> Option<(Vec3, Vec3)> {
    let mut min = Vec3::splat(f32::INFINITY);
    let mut max = Vec3::splat(f32::NEG_INFINITY);
    let mut found = false;
    for vertex in brush
        .faces
        .iter()
        .flat_map(|face| face.vertices.iter().copied())
    {
        if !vertex.is_finite() {
            continue;
        }
        min = min.min(vertex);
        max = max.max(vertex);
        found = true;
    }
    found.then_some((min, max))
}

fn dominant_axis(normal: Vec3) -> Option<(usize, f32)> {
    let abs = normal.abs();
    if abs.max_element() <= 0.0001 {
        return None;
    }
    if abs.x >= abs.y && abs.x >= abs.z {
        Some((0, normal.x.signum()))
    } else if abs.y >= abs.z {
        Some((1, normal.y.signum()))
    } else {
        Some((2, normal.z.signum()))
    }
}

fn world_bounds_for_local_box(min: Vec3, max: Vec3, transform: &Transform) -> (Vec3, Vec3) {
    let affine = transform.compute_affine();
    let mut world_min = Vec3::splat(f32::INFINITY);
    let mut world_max = Vec3::splat(f32::NEG_INFINITY);
    for corner in [
        Vec3::new(min.x, min.y, min.z),
        Vec3::new(max.x, min.y, min.z),
        Vec3::new(max.x, max.y, min.z),
        Vec3::new(min.x, max.y, min.z),
        Vec3::new(min.x, min.y, max.z),
        Vec3::new(max.x, min.y, max.z),
        Vec3::new(max.x, max.y, max.z),
        Vec3::new(min.x, max.y, max.z),
    ] {
        let world = affine.transform_point3(corner);
        world_min = world_min.min(world);
        world_max = world_max.max(world);
    }
    (world_min, world_max)
}

fn draw_clip_preview(gizmos: &mut Gizmos, min: Vec3, max: Vec3) {
    let color = Color::srgba(0.95, 0.35, 1.0, 0.95);
    let corners = [
        Vec3::new(min.x, min.y, min.z),
        Vec3::new(max.x, min.y, min.z),
        Vec3::new(max.x, max.y, min.z),
        Vec3::new(min.x, max.y, min.z),
        Vec3::new(min.x, min.y, max.z),
        Vec3::new(max.x, min.y, max.z),
        Vec3::new(max.x, max.y, max.z),
        Vec3::new(min.x, max.y, max.z),
    ];
    for (a, b) in [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 0),
        (4, 5),
        (5, 6),
        (6, 7),
        (7, 4),
        (0, 4),
        (1, 5),
        (2, 6),
        (3, 7),
    ] {
        gizmos.line(corners[a], corners[b], color);
    }
}

fn selected_brush_entity(
    ui_state: &UiState,
    brushes: &Query<(), With<BrushDesc>>,
) -> Option<Entity> {
    ui_state
        .selected_entities
        .iter()
        .find(|entity| brushes.contains(*entity))
}

fn viewport_ray(
    cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    pointer_pos: egui::Pos2,
    scene_rect: egui::Rect,
) -> Option<Ray3d> {
    let (camera, transform) = cameras
        .iter()
        .find(|(camera, _)| camera.is_active)
        .or_else(|| cameras.iter().next())?;
    scene_view_ray(camera, transform, pointer_pos, scene_rect)
}

fn pick_element(
    mode: BrushEditMode,
    brush: &BrushDesc,
    transform: &GlobalTransform,
    ray: Ray3d,
) -> Option<(BrushElementKey, f32)> {
    match mode {
        BrushEditMode::Vertex => pick_vertex(brush, transform, ray),
        BrushEditMode::Edge => pick_edge(brush, transform, ray),
        BrushEditMode::Face | BrushEditMode::Clip => pick_face(brush, transform, ray),
        BrushEditMode::Object => None,
    }
}

fn selected_anchor_positions(brush: &BrushDesc, selected: &[BrushElementKey]) -> Vec<Vec3> {
    let mut anchors = Vec::new();
    for element in selected {
        match element {
            BrushElementKey::Vertex { face, index } => {
                if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face)
                {
                    if let Some(vertex) = face_desc.vertices.get(*index) {
                        push_unique_anchor(&mut anchors, *vertex);
                    }
                }
            }
            BrushElementKey::Edge { face, index } => {
                if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face)
                {
                    if !face_desc.vertices.is_empty() {
                        let next = (*index + 1) % face_desc.vertices.len();
                        if let Some(vertex) = face_desc.vertices.get(*index) {
                            push_unique_anchor(&mut anchors, *vertex);
                        }
                        if let Some(vertex) = face_desc.vertices.get(next) {
                            push_unique_anchor(&mut anchors, *vertex);
                        }
                    }
                }
            }
            BrushElementKey::Face { face } => {
                if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face)
                {
                    for vertex in &face_desc.vertices {
                        push_unique_anchor(&mut anchors, *vertex);
                    }
                }
            }
        }
    }
    anchors
}

fn push_unique_anchor(anchors: &mut Vec<Vec3>, vertex: Vec3) {
    if !anchors
        .iter()
        .any(|existing| existing.distance_squared(vertex) <= 0.0001)
    {
        anchors.push(vertex);
    }
}

fn recompute_face_planes(brush: &mut BrushDesc) {
    for face in &mut brush.faces {
        let Some(plane) = plane_from_vertices(&face.vertices, face.plane.normal) else {
            continue;
        };
        face.plane = plane;
    }
}

fn plane_from_vertices(vertices: &[Vec3], fallback_normal: Vec3) -> Option<BrushPlaneDesc> {
    if vertices.len() < 3 {
        return None;
    }
    let normal = (vertices[1] - vertices[0])
        .cross(vertices[2] - vertices[0])
        .try_normalize()
        .or_else(|| fallback_normal.try_normalize())?;
    Some(BrushPlaneDesc {
        normal,
        distance: normal.dot(vertices[0]),
    })
}

fn pick_vertex(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    ray: Ray3d,
) -> Option<(BrushElementKey, f32)> {
    let mut best = None;
    for face in &brush.faces {
        for (index, vertex) in world_vertices(face, transform).iter().enumerate() {
            let distance = distance_ray_point(ray, *vertex);
            if distance < 0.25 && best.as_ref().is_none_or(|(_, best)| distance < *best) {
                best = Some((
                    BrushElementKey::Vertex {
                        face: face.id.clone(),
                        index,
                    },
                    distance,
                ));
            }
        }
    }
    best
}

fn pick_edge(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    ray: Ray3d,
) -> Option<(BrushElementKey, f32)> {
    let mut best = None;
    for face in &brush.faces {
        let vertices = world_vertices(face, transform);
        for index in 0..vertices.len() {
            let next = (index + 1) % vertices.len();
            let distance = distance_ray_segment(ray, vertices[index], vertices[next]);
            if distance < 0.2 && best.as_ref().is_none_or(|(_, best)| distance < *best) {
                best = Some((
                    BrushElementKey::Edge {
                        face: face.id.clone(),
                        index,
                    },
                    distance,
                ));
            }
        }
    }
    best
}

fn pick_face(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    ray: Ray3d,
) -> Option<(BrushElementKey, f32)> {
    let mut best = None;
    for face in &brush.faces {
        let vertices = world_vertices(face, transform);
        if vertices.len() < 3 {
            continue;
        }
        let normal = transform
            .affine()
            .transform_vector3(face.plane.normal)
            .normalize_or_zero();
        let denom = normal.dot(ray.direction.as_vec3());
        if denom.abs() < 0.0001 {
            continue;
        }
        let distance = normal.dot(vertices[0] - ray.origin) / denom;
        if distance < 0.0 {
            continue;
        }
        let hit = ray.origin + ray.direction.as_vec3() * distance;
        if point_in_convex_polygon(hit, &vertices, normal)
            && best.as_ref().is_none_or(|(_, best)| distance < *best)
        {
            best = Some((
                BrushElementKey::Face {
                    face: face.id.clone(),
                },
                distance,
            ));
        }
    }
    best
}

pub(crate) fn pick_brush_face_id(
    brush: &BrushDesc,
    transform: &GlobalTransform,
    ray: Ray3d,
) -> Option<(ComponentInstanceId, f32)> {
    let (BrushElementKey::Face { face }, distance) = pick_face(brush, transform, ray)? else {
        return None;
    };
    Some((face, distance))
}

fn world_vertices(face: &BrushFaceDesc, transform: &GlobalTransform) -> Vec<Vec3> {
    face.vertices
        .iter()
        .map(|vertex| transform.transform_point(*vertex))
        .collect()
}

fn average_point(points: &[Vec3]) -> Vec3 {
    points.iter().copied().sum::<Vec3>() / points.len() as f32
}

fn point_in_convex_polygon(point: Vec3, vertices: &[Vec3], normal: Vec3) -> bool {
    let mut expected_sign = 0.0f32;
    for index in 0..vertices.len() {
        let next = (index + 1) % vertices.len();
        let edge = vertices[next] - vertices[index];
        let to_point = point - vertices[index];
        let side = edge.cross(to_point).dot(normal);
        if side.abs() <= 0.0001 {
            continue;
        }
        if expected_sign == 0.0 {
            expected_sign = side.signum();
        } else if side.signum() != expected_sign {
            return false;
        }
    }
    true
}

fn distance_ray_point(ray: Ray3d, point: Vec3) -> f32 {
    let to_point = point - ray.origin;
    let t = to_point.dot(ray.direction.as_vec3()).max(0.0);
    (ray.origin + ray.direction.as_vec3() * t - point).length()
}

fn distance_ray_segment(ray: Ray3d, a: Vec3, b: Vec3) -> f32 {
    let ray_dir = ray.direction.as_vec3();
    let segment = b - a;
    let w0 = ray.origin - a;
    let a_dot = ray_dir.dot(ray_dir);
    let b_dot = ray_dir.dot(segment);
    let c_dot = segment.dot(segment);
    let d_dot = ray_dir.dot(w0);
    let e_dot = segment.dot(w0);
    let denom = a_dot * c_dot - b_dot * b_dot;
    let ray_t = if denom.abs() > 0.0001 {
        ((b_dot * e_dot - c_dot * d_dot) / denom).max(0.0)
    } else {
        0.0
    };
    let segment_t = if c_dot > 0.0001 {
        ((a_dot * e_dot - b_dot * d_dot) / denom.max(0.0001)).clamp(0.0, 1.0)
    } else {
        0.0
    };
    let ray_point = ray.origin + ray_dir * ray_t;
    let segment_point = a + segment * segment_t;
    ray_point.distance(segment_point)
}

fn set_brush_edit_status(
    active_operator: &mut ActiveOperator,
    phase: OperatorPhase,
    hint: impl Into<String>,
) {
    active_operator.status = Some(OperatorStatus {
        id: "brush.edit_mode".to_string(),
        label: "Brush Edit".to_string(),
        phase,
        hint: hint.into(),
        warnings: Vec::new(),
    });
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::history::EditorHistory;
    use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
    use crate::scene_io::SceneIo;
    use crate::selection::SelectedEntity;
    use shared::{BrushDesc, LevelObject};

    #[test]
    fn point_inside_convex_polygon_is_detected() {
        let vertices = vec![
            Vec3::new(-1.0, 0.0, -1.0),
            Vec3::new(1.0, 0.0, -1.0),
            Vec3::new(1.0, 0.0, 1.0),
            Vec3::new(-1.0, 0.0, 1.0),
        ];
        assert!(point_in_convex_polygon(Vec3::ZERO, &vertices, Vec3::Y));
        assert!(!point_in_convex_polygon(
            Vec3::new(2.0, 0.0, 0.0),
            &vertices,
            Vec3::Y
        ));
    }

    #[test]
    fn moving_selected_vertex_moves_matching_welded_corners() {
        let mut brush = BrushDesc::default();
        let selected_vertex = brush.faces[0].vertices[0];
        let before = brush
            .faces
            .iter()
            .flat_map(|face| face.vertices.iter())
            .filter(|vertex| vertex.distance_squared(selected_vertex) <= 0.0001)
            .count();
        let face = brush.faces[0].id.clone();

        let changed = transform_selected_elements_by_gizmo(
            &mut brush,
            &[BrushElementKey::Vertex { face, index: 0 }],
            &GlobalTransform::default(),
            Transform::default(),
            Transform::from_translation(Vec3::new(0.5, 0.0, 0.0)),
        );

        let after = brush
            .faces
            .iter()
            .flat_map(|face| face.vertices.iter())
            .filter(|vertex| vertex.distance_squared(selected_vertex + Vec3::X * 0.5) <= 0.0001)
            .count();
        assert!(changed);
        assert_eq!(before, after);
    }

    #[test]
    fn collapsed_face_move_produces_invalid_brush() {
        let mut brush = BrushDesc::default();
        let face = brush.faces[0].id.clone();
        assert!(transform_selected_elements_by_gizmo(
            &mut brush,
            &[BrushElementKey::Face { face }],
            &GlobalTransform::default(),
            Transform::default(),
            Transform::from_translation(Vec3::new(-1.0, 0.0, 0.0)),
        ));
        assert!(!validate_brush(&brush).is_valid());
    }

    #[test]
    fn clip_preview_keeps_half_along_selected_face_normal() {
        let brush = BrushDesc::default();
        let face = brush.faces[0].id.clone();
        let result = clip_preview_result(&brush, &Transform::default(), &face).expect("clip");
        let (min, max) = local_brush_bounds(&result.brush).expect("bounds");
        assert_eq!(max - min, Vec3::new(0.5, 1.0, 1.0));
        assert_eq!(result.transform.translation, Vec3::new(0.25, 0.0, 0.0));
        assert!(validate_brush(&result.brush).is_valid());
    }

    #[test]
    fn clip_history_command_is_grouped() {
        let command = EditorCommand::SetBrushTransform {
            entity: Entity::PLACEHOLDER,
            old_transform: Transform::default(),
            new_transform: Transform::from_translation(Vec3::X),
            old_brush: Some(BrushDesc::default()),
            new_brush: BrushDesc::default(),
        };

        assert_eq!(command.label(), "Clip Brush");
    }

    #[test]
    fn clip_commit_is_one_undo_group_and_round_trips_geometry() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<SelectedEntity>();
        let initial_transform = Transform::default();
        let initial_brush = BrushDesc::default();
        let entity = world
            .spawn((LevelObject, initial_transform, initial_brush.clone()))
            .id();
        let face = initial_brush.faces[0].id.clone();
        let result = clip_preview_result(&initial_brush, &initial_transform, &face).unwrap();
        let committed = (result.transform, result.brush.clone());
        let harness = OperatorInvariantHarness::capture(&mut world);

        set_brush_transform_with_history(&mut world, entity, result.transform, result.brush);
        set_brush_edit_status(
            &mut world.resource_mut::<ActiveOperator>(),
            OperatorPhase::Committed,
            "Brush clip committed",
        );

        harness.assert_committed(&mut world, 1, 0);
        assert_undo_redo_round_trip(
            &mut world,
            (initial_transform, initial_brush),
            committed,
            |world| {
                (
                    *world.get::<Transform>(entity).unwrap(),
                    world.get::<BrushDesc>(entity).unwrap().clone(),
                )
            },
        );
    }

    #[test]
    fn escape_from_element_preview_restores_brush_and_despawns_helper() {
        let mut app = App::new();
        app.init_resource::<ActiveOperator>()
            .init_resource::<EditorHistory>()
            .init_resource::<SceneIo>()
            .init_resource::<BrushEditMode>()
            .init_resource::<BrushElementSelection>()
            .init_resource::<BrushElementGizmoState>()
            .add_systems(Update, sync_brush_element_gizmo);
        let initial = BrushDesc::default();
        let mut preview = initial.clone();
        preview.faces[0].vertices[0] += Vec3::X * 0.25;
        let brush_entity = app
            .world_mut()
            .spawn((LevelObject, preview, GlobalTransform::default()))
            .id();
        let helper = app.world_mut().spawn(BrushElementGizmo).id();
        *app.world_mut().resource_mut::<BrushEditMode>() = BrushEditMode::Object;
        *app.world_mut().resource_mut::<BrushElementGizmoState>() = BrushElementGizmoState {
            entity: Some(helper),
            brush: Some(brush_entity),
            old_brush: Some(initial.clone()),
            old_gizmo: Some(Transform::default()),
            last_gizmo: Some(Transform::from_translation(Vec3::X)),
            changed: true,
        };
        set_brush_edit_status(
            &mut app.world_mut().resource_mut::<ActiveOperator>(),
            OperatorPhase::Canceled,
            "Object mode",
        );
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        app.update();

        harness.assert_canceled(app.world_mut());
        assert_eq!(app.world().get::<BrushDesc>(brush_entity), Some(&initial));
        harness.assert_no_helpers::<BrushElementGizmo>(app.world_mut());
        let state = app.world().resource::<BrushElementGizmoState>();
        assert!(state.entity.is_none());
        assert!(state.old_brush.is_none());
        assert!(!state.changed);
    }
}
