//! MVP brush CSG operations for convex cuboid/prism blockout brushes.

use bevy::math::Affine3A;
use bevy::prelude::*;
use shared::{
    brush_math::{validate_brush, BrushDiagnosticSeverity},
    BrushDesc, LevelObject,
};

use crate::history::apply_brush_csg_with_history;
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
use crate::scene_io::SceneIo;
use crate::ui::UiState;

pub struct BrushCsgPlugin;

impl Plugin for BrushCsgPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<BrushCsgPreview>().add_systems(
            Update,
            (brush_csg_preview_input, draw_brush_csg_preview)
                .run_if(crate::state::scene_tools_active),
        );
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct BrushBounds {
    min: Vec3,
    max: Vec3,
}

impl BrushBounds {
    fn union(self, other: Self) -> Self {
        Self {
            min: self.min.min(other.min),
            max: self.max.max(other.max),
        }
    }

    fn intersection(self, other: Self) -> Option<Self> {
        let min = self.min.max(other.min);
        let max = self.max.min(other.max);
        ((max.x - min.x) > 0.001 && (max.y - min.y) > 0.001 && (max.z - min.z) > 0.001)
            .then_some(Self { min, max })
    }

    fn center(self) -> Vec3 {
        (self.min + self.max) * 0.5
    }

    fn size(self) -> Vec3 {
        self.max - self.min
    }

    fn volume(self) -> f32 {
        let size = self.size();
        size.x.max(0.0) * size.y.max(0.0) * size.z.max(0.0)
    }
}

pub fn selected_brush_count(world: &World) -> usize {
    world
        .resource::<UiState>()
        .selected_entities
        .iter()
        .filter(|entity| world.get::<BrushDesc>(*entity).is_some())
        .count()
}

pub fn intersect_selected_brushes(world: &mut World) {
    run_bounds_csg(world, BrushCsgOp::Intersect);
}

pub fn merge_selected_brushes(world: &mut World) {
    run_bounds_csg(world, BrushCsgOp::Merge);
}

pub fn subtract_selected_brushes(world: &mut World) {
    run_bounds_csg(world, BrushCsgOp::Subtract);
}

#[derive(Resource, Default, Debug, Clone)]
struct BrushCsgPreview {
    pending: Option<PendingBrushCsg>,
}

#[derive(Debug, Clone)]
struct PendingBrushCsg {
    op: BrushCsgOp,
    selected: Vec<Entity>,
    result: BrushBounds,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BrushCsgOp {
    Intersect,
    Merge,
    Subtract,
}

fn run_bounds_csg(world: &mut World, op: BrushCsgOp) {
    let selected: Vec<Entity> = world
        .resource::<UiState>()
        .selected_entities
        .iter()
        .filter(|entity| {
            world.get::<LevelObject>(*entity).is_some() && world.get::<BrushDesc>(*entity).is_some()
        })
        .collect();
    if selected.len() < 2 {
        set_status(world, "Select at least two brushes for CSG");
        return;
    }
    for entity in &selected {
        let Some(brush) = world.get::<BrushDesc>(*entity) else {
            continue;
        };
        let validation = validate_brush(brush);
        if !validation.is_valid() {
            set_status(
                world,
                format!(
                    "CSG failed: selected brush is invalid: {}",
                    first_brush_error(&validation)
                ),
            );
            return;
        }
    }

    let Some(primary_bounds) = brush_world_bounds(world, selected[0]) else {
        set_status(world, "Primary brush has no valid bounds");
        return;
    };

    let result = match op {
        BrushCsgOp::Merge => selected
            .iter()
            .skip(1)
            .filter_map(|entity| brush_world_bounds(world, *entity))
            .fold(primary_bounds, BrushBounds::union),
        BrushCsgOp::Intersect => {
            let mut result = primary_bounds;
            for entity in selected.iter().skip(1) {
                let Some(bounds) = brush_world_bounds(world, *entity) else {
                    continue;
                };
                let Some(intersection) = result.intersection(bounds) else {
                    set_status(world, "Brush intersect failed: no overlap");
                    return;
                };
                result = intersection;
            }
            result
        }
        BrushCsgOp::Subtract => {
            let Some(cutter_bounds) = brush_world_bounds(world, selected[1]) else {
                set_status(world, "Subtract failed: cutter brush has no valid bounds");
                return;
            };
            let Some(overlap) = primary_bounds.intersection(cutter_bounds) else {
                set_status(world, "Subtract failed: brushes do not overlap");
                return;
            };
            let Some(result) = largest_remaining_slab(primary_bounds, overlap) else {
                set_status(world, "Subtract failed: result would be empty");
                return;
            };
            result
        }
    };

    let size = result.size();
    let new_brush = BrushDesc::cuboid(size);
    let validation = validate_brush(&new_brush);
    if !validation.is_valid() {
        set_status(
            world,
            format!(
                "CSG failed: result is invalid: {}",
                first_brush_error(&validation)
            ),
        );
        return;
    }

    world.resource_mut::<BrushCsgPreview>().pending = Some(PendingBrushCsg {
        op,
        selected,
        result,
    });
    let hint = match op {
        BrushCsgOp::Intersect => "Brush intersect preview: Enter commits, Esc cancels",
        BrushCsgOp::Merge => "Brush convex merge preview: Enter commits, Esc cancels",
        BrushCsgOp::Subtract => "Brush subtract preview: Enter commits, Esc cancels",
    };
    set_status(world, hint);
    set_csg_operator_status(world, OperatorPhase::Preview, hint);
}

fn brush_csg_preview_input(world: &mut World) {
    let enter = world
        .get_resource::<ButtonInput<KeyCode>>()
        .is_some_and(|keys| keys.just_pressed(KeyCode::Enter));
    let escape = world
        .get_resource::<ButtonInput<KeyCode>>()
        .is_some_and(|keys| keys.just_pressed(KeyCode::Escape));
    if !enter && !escape {
        return;
    }
    if escape {
        cancel_csg_preview(world);
        return;
    }
    let Some(pending) = world.resource_mut::<BrushCsgPreview>().pending.take() else {
        return;
    };
    commit_csg_preview(world, pending);
}

fn cancel_csg_preview(world: &mut World) {
    if world
        .resource_mut::<BrushCsgPreview>()
        .pending
        .take()
        .is_some()
    {
        set_status(world, "Brush CSG preview canceled");
        set_csg_operator_status(world, OperatorPhase::Canceled, "Canceled");
    }
}

fn commit_csg_preview(world: &mut World, pending: PendingBrushCsg) {
    let Some(primary) = pending.selected.first().copied() else {
        set_status(world, "Brush CSG commit failed: missing primary brush");
        set_csg_operator_status(
            world,
            OperatorPhase::Blocked,
            "Commit failed: missing primary brush",
        );
        return;
    };
    if world.get::<BrushDesc>(primary).is_none() {
        set_status(
            world,
            "Brush CSG commit failed: primary brush no longer exists",
        );
        set_csg_operator_status(
            world,
            OperatorPhase::Blocked,
            "Commit failed: primary brush no longer exists",
        );
        return;
    }
    let new_brush = BrushDesc::cuboid(pending.result.size());
    let to_delete: Vec<_> = if matches!(pending.op, BrushCsgOp::Merge | BrushCsgOp::Intersect) {
        pending
            .selected
            .iter()
            .skip(1)
            .copied()
            .filter(|entity| world.get::<BrushDesc>(*entity).is_some())
            .collect()
    } else {
        Vec::new()
    };
    apply_bounds_result(world, primary, pending.result, new_brush, &to_delete);
    let hint = match pending.op {
        BrushCsgOp::Intersect => "Brush intersect committed",
        BrushCsgOp::Merge => "Brush convex merge committed",
        BrushCsgOp::Subtract => "Brush subtract committed",
    };
    set_status(world, hint);
    set_csg_operator_status(world, OperatorPhase::Committed, hint);
}

fn draw_brush_csg_preview(preview: Res<BrushCsgPreview>, mut gizmos: Gizmos) {
    let Some(pending) = preview.pending.as_ref() else {
        return;
    };
    draw_bounds_box(
        &mut gizmos,
        pending.result,
        Color::srgba(0.25, 0.85, 1.0, 0.95),
    );
}

fn draw_bounds_box(gizmos: &mut Gizmos, bounds: BrushBounds, color: Color) {
    let min = bounds.min;
    let max = bounds.max;
    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 first_brush_error(report: &shared::brush_math::BrushValidationReport) -> &str {
    report
        .diagnostics
        .iter()
        .find(|diagnostic| diagnostic.severity == BrushDiagnosticSeverity::Error)
        .map(|diagnostic| diagnostic.message.as_str())
        .unwrap_or("Brush geometry is invalid.")
}

fn apply_bounds_result(
    world: &mut World,
    entity: Entity,
    bounds: BrushBounds,
    new_brush: BrushDesc,
    delete_entities: &[Entity],
) {
    let center = bounds.center();
    let old_transform = world.get::<Transform>(entity).copied().unwrap_or_default();
    let mut new_transform = old_transform;
    new_transform.translation = center;
    new_transform.rotation = Quat::IDENTITY;
    new_transform.scale = Vec3::ONE;
    apply_brush_csg_with_history(world, entity, new_transform, new_brush, delete_entities);
}

fn brush_world_bounds(world: &World, entity: Entity) -> Option<BrushBounds> {
    let brush = world.get::<BrushDesc>(entity)?;
    let transform = world
        .get::<GlobalTransform>(entity)
        .map(|global| global.affine())
        .or_else(|| {
            world
                .get::<Transform>(entity)
                .map(|transform| transform.compute_affine())
        })
        .unwrap_or(Affine3A::IDENTITY);
    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())
    {
        let world_vertex = transform.transform_point3(vertex);
        min = min.min(world_vertex);
        max = max.max(world_vertex);
        found = true;
    }
    found.then_some(BrushBounds { min, max })
}

fn largest_remaining_slab(source: BrushBounds, overlap: BrushBounds) -> Option<BrushBounds> {
    let candidates = [
        BrushBounds {
            min: source.min,
            max: Vec3::new(overlap.min.x, source.max.y, source.max.z),
        },
        BrushBounds {
            min: Vec3::new(overlap.max.x, source.min.y, source.min.z),
            max: source.max,
        },
        BrushBounds {
            min: source.min,
            max: Vec3::new(source.max.x, overlap.min.y, source.max.z),
        },
        BrushBounds {
            min: Vec3::new(source.min.x, overlap.max.y, source.min.z),
            max: source.max,
        },
        BrushBounds {
            min: source.min,
            max: Vec3::new(source.max.x, source.max.y, overlap.min.z),
        },
        BrushBounds {
            min: Vec3::new(source.min.x, source.min.y, overlap.max.z),
            max: source.max,
        },
    ];
    candidates
        .into_iter()
        .filter(|bounds| bounds.size().cmpgt(Vec3::splat(0.001)).all())
        .max_by(|a, b| a.volume().total_cmp(&b.volume()))
}

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

fn set_csg_operator_status(world: &mut World, phase: OperatorPhase, hint: impl Into<String>) {
    if let Some(mut active) = world.get_resource_mut::<ActiveOperator>() {
        active.status = Some(OperatorStatus {
            id: "brush.csg".to_string(),
            label: "Brush CSG".to_string(),
            phase,
            hint: hint.into(),
            warnings: Vec::new(),
        });
    }
}

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

    fn bounds(min: Vec3, max: Vec3) -> BrushBounds {
        BrushBounds { min, max }
    }

    #[test]
    fn intersects_overlapping_bounds() {
        let a = bounds(Vec3::ZERO, Vec3::splat(2.0));
        let b = bounds(Vec3::ONE, Vec3::splat(3.0));
        assert_eq!(a.intersection(b), Some(bounds(Vec3::ONE, Vec3::splat(2.0))));
    }

    #[test]
    fn subtract_picks_largest_remaining_slab() {
        let source = bounds(Vec3::ZERO, Vec3::new(4.0, 2.0, 2.0));
        let overlap = bounds(Vec3::ZERO, Vec3::new(1.0, 2.0, 2.0));
        let result = largest_remaining_slab(source, overlap).expect("remaining slab");
        assert_eq!(result.min, Vec3::new(1.0, 0.0, 0.0));
        assert_eq!(result.max, source.max);
    }

    #[test]
    fn csg_history_command_is_grouped() {
        let command = EditorCommand::ApplyBrushCsg {
            primary: Entity::PLACEHOLDER,
            old_transform: Transform::default(),
            new_transform: Transform::from_translation(Vec3::X),
            old_brush: Some(BrushDesc::default()),
            new_brush: BrushDesc::default(),
            deleted: Vec::new(),
        };

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

    #[test]
    fn csg_preview_cancel_clears_pending_without_authored_side_effects() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<BrushCsgPreview>();
        world.resource_mut::<BrushCsgPreview>().pending = Some(PendingBrushCsg {
            op: BrushCsgOp::Merge,
            selected: vec![Entity::PLACEHOLDER],
            result: bounds(Vec3::ZERO, Vec3::ONE),
        });
        set_csg_operator_status(
            &mut world,
            OperatorPhase::Preview,
            "Enter commits, Esc cancels",
        );
        let harness = OperatorInvariantHarness::capture(&mut world);

        cancel_csg_preview(&mut world);

        harness.assert_canceled(&mut world);
        assert!(world.resource::<BrushCsgPreview>().pending.is_none());
    }

    #[test]
    fn csg_merge_commit_is_one_undo_group_and_round_trips_deleted_brush() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<SelectedEntity>();
        let primary = world
            .spawn((LevelObject, BrushDesc::default(), Transform::default()))
            .id();
        let secondary = world
            .spawn((
                LevelObject,
                BrushDesc::default(),
                Transform::from_translation(Vec3::new(0.5, 0.0, 0.0)),
            ))
            .id();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let pending = PendingBrushCsg {
            op: BrushCsgOp::Merge,
            selected: vec![primary, secondary],
            result: bounds(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(1.0, 0.5, 0.5)),
        };

        commit_csg_preview(&mut world, pending);

        harness.assert_committed(&mut world, 1, -1);
        assert_undo_redo_round_trip(&mut world, 2, 1, |world| {
            let mut query = world.query_filtered::<Entity, With<LevelObject>>();
            query.iter(world).count()
        });
    }
}
