//! Modal floor-polygon brush drawing tool.

use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use shared::{
    brush_math::{decompose_floor_polygon_to_convex, BrushPolygonError},
    ActorKind, BrushDesc, EditorVisibility, MaterialDesc,
};

use crate::camera::EditorCamera;
use crate::history::{spawn_many_with_history, EditorEntitySnapshot};
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, snap_translation, ViewportDisplayMode, ViewportSettings};

const DEFAULT_BRUSH_HEIGHT: f32 = 2.5;
const MIN_BRUSH_HEIGHT: f32 = 0.1;
const HEIGHT_PIXELS_TO_WORLD: f32 = 0.03;
const MIN_COMMIT_VERTICES: usize = 3;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrushToolPhase {
    Outline,
    Height,
}

#[derive(Resource, Debug, Clone)]
pub struct BrushToolState {
    pub active: bool,
    pub phase: BrushToolPhase,
    pub vertices: Vec<Vec3>,
    pub height: f32,
    height_anchor_pointer_y: Option<f32>,
    height_anchor_value: f32,
}

impl Default for BrushToolState {
    fn default() -> Self {
        Self {
            active: false,
            phase: BrushToolPhase::Outline,
            vertices: Vec::new(),
            height: DEFAULT_BRUSH_HEIGHT,
            height_anchor_pointer_y: None,
            height_anchor_value: DEFAULT_BRUSH_HEIGHT,
        }
    }
}

impl BrushToolState {
    pub fn start(&mut self) {
        self.active = true;
        self.phase = BrushToolPhase::Outline;
        self.vertices.clear();
        self.height = DEFAULT_BRUSH_HEIGHT;
        self.height_anchor_pointer_y = None;
        self.height_anchor_value = DEFAULT_BRUSH_HEIGHT;
    }

    pub fn cancel(&mut self) {
        self.active = false;
        self.phase = BrushToolPhase::Outline;
        self.vertices.clear();
        self.height = DEFAULT_BRUSH_HEIGHT;
        self.height_anchor_pointer_y = None;
        self.height_anchor_value = DEFAULT_BRUSH_HEIGHT;
    }

    fn start_height_phase(&mut self, pointer_pos: Option<egui::Pos2>) {
        self.phase = BrushToolPhase::Height;
        self.height = DEFAULT_BRUSH_HEIGHT;
        self.height_anchor_value = DEFAULT_BRUSH_HEIGHT;
        self.height_anchor_pointer_y = pointer_pos.map(|pos| pos.y);
    }

    fn return_to_outline_phase(&mut self) {
        self.phase = BrushToolPhase::Outline;
        self.height = DEFAULT_BRUSH_HEIGHT;
        self.height_anchor_pointer_y = None;
        self.height_anchor_value = DEFAULT_BRUSH_HEIGHT;
    }

    fn update_height_from_pointer(&mut self, pointer_pos: Option<egui::Pos2>) {
        if self.phase != BrushToolPhase::Height {
            return;
        }
        let Some(pointer_pos) = pointer_pos else {
            return;
        };
        let anchor_y = self.height_anchor_pointer_y.get_or_insert(pointer_pos.y);
        let height =
            self.height_anchor_value + (*anchor_y - pointer_pos.y) * HEIGHT_PIXELS_TO_WORLD;
        self.height = quantize_height(height);
    }
}

pub struct BrushToolPlugin;

impl Plugin for BrushToolPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<BrushToolState>().add_systems(
            Update,
            (brush_tool_input, draw_brush_tool_preview).run_if(scene_tools_active),
        );
    }
}

pub fn start_draw_brush_tool(world: &mut World) {
    world.resource_mut::<BrushToolState>().start();
    world.resource_mut::<SceneIo>().status =
        "Draw Brush: click floor points, Enter sets height".to_string();
    if let Some(mut active_operator) = world.get_resource_mut::<ActiveOperator>() {
        set_brush_tool_status(
            &mut active_operator,
            OperatorPhase::Preview,
            "Click floor points, Enter sets height, Esc cancels",
        );
    }
    if let Some(mut viewport_click) = world.get_resource_mut::<ViewportClick>() {
        viewport_click.0 = None;
    }
}

#[allow(clippy::too_many_arguments)]
fn brush_tool_input(
    mut commands: Commands,
    mut tool: ResMut<BrushToolState>,
    mut viewport_click: ResMut<ViewportClick>,
    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>,
    settings: Res<ViewportSettings>,
    cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
) -> Result {
    if display.clean_game_view {
        if tool.active {
            tool.cancel();
            set_brush_tool_status(&mut active_operator, OperatorPhase::Canceled, "Canceled");
        }
        return Ok(());
    }

    let ctx = contexts.ctx_mut()?;
    let egui_keyboard_busy = ctx.egui_wants_keyboard_input();
    let keyboard_available =
        viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) && !egui_keyboard_busy;

    if keyboard_available && keys.just_pressed(KeyCode::KeyB) {
        tool.start();
        scene_io.status = "Draw Brush: click floor points, Enter sets height".to_string();
        set_brush_tool_status(
            &mut active_operator,
            OperatorPhase::Preview,
            "Click floor points, Enter sets height, Esc cancels",
        );
        viewport_click.0 = None;
        return Ok(());
    }

    if !tool.active {
        return Ok(());
    }
    tool.update_height_from_pointer(ui_state.viewport_pointer_pos);

    if (!egui_keyboard_busy && keys.just_pressed(KeyCode::Escape))
        || buttons.just_pressed(MouseButton::Right)
    {
        tool.cancel();
        viewport_click.0 = None;
        scene_io.status = "Draw Brush cancelled".to_string();
        set_brush_tool_status(&mut active_operator, OperatorPhase::Canceled, "Canceled");
        return Ok(());
    }

    if !egui_keyboard_busy && keys.just_pressed(KeyCode::Backspace) {
        if tool.phase == BrushToolPhase::Height {
            tool.return_to_outline_phase();
        } else {
            tool.vertices.pop();
        }
        let status = draw_brush_status(&tool);
        scene_io.status = status.clone();
        set_brush_tool_status(&mut active_operator, OperatorPhase::Preview, status);
        viewport_click.0 = None;
        return Ok(());
    }

    if tool.phase == BrushToolPhase::Height {
        if buttons.just_pressed(MouseButton::Left)
            || (!egui_keyboard_busy && keys.just_pressed(KeyCode::Enter))
        {
            match brush_snapshots_from_points(&tool.vertices, tool.height) {
                Ok(snapshots) => {
                    let brush_count = snapshots.len();
                    let height = tool.height;
                    commands.queue(move |world: &mut World| {
                        spawn_many_with_history(world, snapshots);
                    });
                    tool.cancel();
                    scene_io.status = brush_created_status(brush_count, height);
                    set_brush_tool_status(
                        &mut active_operator,
                        OperatorPhase::Committed,
                        brush_committed_status(brush_count, height),
                    );
                }
                Err(error) => {
                    let message = format!("Draw Brush failed: {error}");
                    warn!("{message}; points={:?}", tool.vertices);
                    scene_io.status = message.clone();
                    set_brush_tool_status(&mut active_operator, OperatorPhase::Blocked, message);
                }
            }
            viewport_click.0 = None;
            return Ok(());
        }
        viewport_click.0 = None;
        let status = draw_brush_status(&tool);
        scene_io.status = status.clone();
        set_brush_tool_status(&mut active_operator, OperatorPhase::Preview, status);
        return Ok(());
    }

    if let Some(pointer_pos) = viewport_click.0.take() {
        if let Some(mut point) =
            pointer_floor_position(&cameras, pointer_pos, ui_state.viewport_rect)
        {
            point = snap_translation(point, &settings);
            tool.vertices.push(point);
            let status = draw_brush_status(&tool);
            scene_io.status = status.clone();
            set_brush_tool_status(&mut active_operator, OperatorPhase::Preview, status);
        }
    }

    if !egui_keyboard_busy && keys.just_pressed(KeyCode::Enter) {
        match decompose_floor_polygon_to_convex(&tool.vertices) {
            Ok(parts) => {
                let part_count = parts.len();
                tool.start_height_phase(ui_state.viewport_pointer_pos);
                scene_io.status = draw_brush_status(&tool);
                set_brush_tool_status(
                    &mut active_operator,
                    OperatorPhase::Preview,
                    height_phase_status(part_count, tool.height),
                );
            }
            Err(error) => {
                let message = format!("Draw Brush failed: {}", floor_polygon_error_message(error));
                warn!("{message}; points={:?}", tool.vertices);
                scene_io.status = message.clone();
                set_brush_tool_status(&mut active_operator, OperatorPhase::Blocked, message);
            }
        }
    }

    Ok(())
}

fn draw_brush_tool_preview(
    tool: Res<BrushToolState>,
    ui_state: Res<UiState>,
    settings: Res<ViewportSettings>,
    cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    mut gizmos: Gizmos,
) {
    if !tool.active {
        return;
    }
    let mut points = tool.vertices.clone();
    if tool.phase == BrushToolPhase::Outline {
        if let Some(pointer_pos) = ui_state.viewport_pointer_pos {
            if let Some(mut point) =
                pointer_floor_position(&cameras, pointer_pos, ui_state.viewport_rect)
            {
                point = snap_translation(point, &settings);
                points.push(point);
            }
        }
    }
    if points.is_empty() {
        return;
    }
    let color = brush_preview_color(&points);
    for point in &points {
        gizmos.sphere(*point + Vec3::Y * 0.02, 0.08, color);
        gizmos.line(*point, *point + Vec3::Y * tool.height, color);
    }
    for window in points.windows(2) {
        gizmos.line(window[0], window[1], color);
        gizmos.line(
            window[0] + Vec3::Y * tool.height,
            window[1] + Vec3::Y * tool.height,
            color,
        );
    }
    if points.len() >= MIN_COMMIT_VERTICES {
        let first = points[0];
        let last = *points.last().expect("points checked non-empty");
        gizmos.line(last, first, color);
        gizmos.line(
            last + Vec3::Y * tool.height,
            first + Vec3::Y * tool.height,
            color,
        );
        if let Ok(parts) = decompose_floor_polygon_to_convex(&points) {
            if parts.len() > 1 {
                draw_decomposition_preview(&parts, tool.height, &mut gizmos);
            }
        }
    }
}

fn pointer_floor_position(
    cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    pointer_pos: egui::Pos2,
    scene_rect: egui::Rect,
) -> Option<Vec3> {
    let (camera, transform) = cameras
        .iter()
        .find(|(camera, _)| camera.is_active)
        .or_else(|| cameras.iter().next())?;
    let ray = scene_view_ray(camera, transform, pointer_pos, scene_rect)?;
    let direction = ray.direction.as_vec3();
    if direction.y.abs() < 0.0001 {
        return Some(ray.origin);
    }
    let t = -ray.origin.y / direction.y;
    Some(ray.origin + direction * t.max(0.0))
}

fn brush_snapshots_from_points(
    points: &[Vec3],
    height: f32,
) -> Result<Vec<EditorEntitySnapshot>, String> {
    let parts = decompose_floor_polygon_to_convex(points).map_err(floor_polygon_error_message)?;
    let part_count = parts.len();
    parts
        .iter()
        .enumerate()
        .map(|(index, part)| {
            let name = if part_count == 1 {
                "Brush".to_string()
            } else {
                format!("Brush Part {}", index + 1)
            };
            brush_snapshot_from_convex_points(part, height, name)
        })
        .collect()
}

fn brush_snapshot_from_convex_points(
    points: &[Vec3],
    height: f32,
    name: String,
) -> Result<EditorEntitySnapshot, String> {
    let center = polygon_center(points);
    let local_points: Vec<Vec3> = points.iter().map(|point| *point - center).collect();
    let brush = BrushDesc::extruded_prism(&local_points, height)
        .ok_or_else(|| "invalid brush prism".to_string())?;
    Ok(EditorEntitySnapshot {
        actor_id: None,
        actor_kind: ActorKind::Brush,
        actor_name: None,
        name: Some(name),
        transform: Transform::from_translation(center),
        primitive: None,
        brush: Some(brush),
        static_mesh_renderer: None,
        skinned_mesh_renderer: None,
        material: Some(MaterialDesc::default()),
        material_override: None,
        rigid_body: None,
        collider: None,
        physics: None,
        light: None,
        animation_controller: None,
        audio_source: None,
        audio_listener: None,
        player_spawn: false,
        model: None,
        prefab: None,
        prefab_instance: None,
        weapon_spawn: None,
        trigger_volume: None,
        post_process_volume: None,
        team_spawn: None,
        objective: None,

        navigation_bounds: None,

        navigation_obstacle: None,

        navigation_area: None,

        navigation_link: None,
        hierarchy_sibling_index: 0,
        editor_visibility: EditorVisibility::default(),
        inspector_order: None,
        component_states: None,
        children: Vec::new(),
    })
}

fn polygon_center(points: &[Vec3]) -> Vec3 {
    let sum = points
        .iter()
        .copied()
        .fold(Vec3::ZERO, |sum, point| sum + point);
    sum / points.len() as f32
}

fn quantize_height(height: f32) -> f32 {
    ((height.max(MIN_BRUSH_HEIGHT) * 10.0).round() / 10.0).max(MIN_BRUSH_HEIGHT)
}

fn brush_tool_hint(vertex_count: usize) -> String {
    match vertex_count {
        0 => "Click first floor point".to_string(),
        1 => "Click second floor point".to_string(),
        2 => "Click third floor point".to_string(),
        count => format!("{count} points, Enter sets height"),
    }
}

fn draw_brush_status(tool: &BrushToolState) -> String {
    if tool.phase == BrushToolPhase::Height {
        let part_count = decompose_floor_polygon_to_convex(&tool.vertices)
            .map(|parts| parts.len())
            .unwrap_or(1);
        return height_phase_status(part_count, tool.height);
    }
    let points = &tool.vertices;
    if points.len() < MIN_COMMIT_VERTICES {
        return format!("Draw Brush: {}", brush_tool_hint(points.len()));
    }
    match decompose_floor_polygon_to_convex(points) {
        Ok(parts) if parts.len() == 1 => {
            format!("Draw Brush: {} points, Enter creates brush", points.len())
        }
        Ok(parts) => format!(
            "Draw Brush: {} points, Enter creates {} convex brush parts",
            points.len(),
            parts.len()
        ),
        Err(error) => format!("Draw Brush blocked: {}", floor_polygon_error_message(error)),
    }
}

fn height_phase_status(part_count: usize, height: f32) -> String {
    if part_count == 1 {
        format!("Draw Brush height: {height:.1}m, Enter/LMB creates brush")
    } else {
        format!("Draw Brush height: {height:.1}m, Enter/LMB creates {part_count} convex parts")
    }
}

fn brush_created_status(brush_count: usize, height: f32) -> String {
    if brush_count == 1 {
        format!("Brush created at {height:.1}m")
    } else {
        format!("Brush created as {brush_count} convex parts at {height:.1}m")
    }
}

fn brush_committed_status(brush_count: usize, height: f32) -> String {
    if brush_count == 1 {
        format!("Committed brush.draw ({height:.1}m)")
    } else {
        format!("Committed brush.draw ({brush_count} parts, {height:.1}m)")
    }
}

fn brush_preview_color(points: &[Vec3]) -> Color {
    if points.len() < MIN_COMMIT_VERTICES {
        return Color::srgb(0.2, 0.72, 1.0);
    }
    match decompose_floor_polygon_to_convex(points) {
        Ok(_) => Color::srgb(0.2, 0.72, 1.0),
        Err(_) => Color::srgb(1.0, 0.25, 0.2),
    }
}

fn draw_decomposition_preview(parts: &[Vec<Vec3>], height: f32, gizmos: &mut Gizmos) {
    let color = Color::srgb(0.15, 1.0, 0.65);
    for part in parts {
        for window in part.windows(2) {
            gizmos.line(
                window[0] + Vec3::Y * 0.04,
                window[1] + Vec3::Y * 0.04,
                color,
            );
            gizmos.line(
                window[0] + Vec3::Y * height,
                window[1] + Vec3::Y * height,
                color,
            );
        }
        if let (Some(first), Some(last)) = (part.first(), part.last()) {
            gizmos.line(*last + Vec3::Y * 0.04, *first + Vec3::Y * 0.04, color);
            gizmos.line(*last + Vec3::Y * height, *first + Vec3::Y * height, color);
        }
    }
}

fn floor_polygon_error_message(error: BrushPolygonError) -> String {
    match error {
        BrushPolygonError::TooFewVertices => {
            "need at least three points before creating a brush".into()
        }
        BrushPolygonError::NonFiniteVertex => "one or more points are not finite".into(),
        BrushPolygonError::DuplicateVertex => {
            "two points overlap; remove or move the duplicate point".into()
        }
        BrushPolygonError::ZeroArea => "polygon area is zero; points must enclose an area".into(),
        BrushPolygonError::SelfIntersecting => {
            "polygon edges cross; draw points around the perimeter in order".into()
        }
        BrushPolygonError::NonConvex => {
            "concave outline could not be decomposed into convex brush parts".into()
        }
    }
}

fn set_brush_tool_status(
    active_operator: &mut ActiveOperator,
    phase: OperatorPhase,
    hint: impl Into<String>,
) {
    active_operator.status = Some(OperatorStatus {
        id: "brush.draw".to_string(),
        label: "Draw Brush".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::selection::SelectedEntity;
    use shared::LevelObject;

    #[test]
    fn brush_snapshot_centers_polygon_points() {
        let snapshots = brush_snapshots_from_points(
            &[
                Vec3::new(0.0, 0.0, 0.0),
                Vec3::new(2.0, 0.0, 0.0),
                Vec3::new(2.0, 0.0, 2.0),
                Vec3::new(0.0, 0.0, 2.0),
            ],
            3.0,
        )
        .expect("valid brush");
        assert_eq!(snapshots.len(), 1);
        let snapshot = &snapshots[0];

        assert_eq!(snapshot.actor_kind, ActorKind::Brush);
        assert_eq!(snapshot.transform.translation, Vec3::new(1.0, 0.0, 1.0));
        assert!(snapshot.brush.is_some());
    }

    #[test]
    fn brush_snapshot_decomposes_non_convex_polygon() {
        let snapshots = brush_snapshots_from_points(
            &[
                Vec3::new(-1.0, 0.0, -1.0),
                Vec3::new(1.0, 0.0, -1.0),
                Vec3::new(0.0, 0.0, 0.0),
                Vec3::new(1.0, 0.0, 1.0),
                Vec3::new(-1.0, 0.0, 1.0),
            ],
            2.0,
        )
        .expect("concave brush parts");

        assert_eq!(snapshots.len(), 2);
        assert!(snapshots.iter().all(|snapshot| snapshot.brush.is_some()));
    }

    #[test]
    fn brush_snapshot_reports_actionable_invalid_polygon_reason() {
        let result = brush_snapshots_from_points(
            &[
                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),
            ],
            2.0,
        );

        assert_eq!(
            result.unwrap_err(),
            "polygon edges cross; draw points around the perimeter in order"
        );
    }

    #[test]
    fn draw_brush_status_explains_invalid_shape() {
        let tool = BrushToolState {
            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),
            ],
            ..Default::default()
        };
        let status = draw_brush_status(&tool);

        assert!(status.contains("polygon edges cross"));
    }

    #[test]
    fn height_phase_status_reports_height_and_part_count() {
        let mut tool = BrushToolState {
            active: true,
            phase: BrushToolPhase::Height,
            height: 4.2,
            vertices: vec![
                Vec3::new(-1.0, 0.0, -1.0),
                Vec3::new(1.0, 0.0, -1.0),
                Vec3::new(0.0, 0.0, 0.0),
                Vec3::new(1.0, 0.0, 1.0),
                Vec3::new(-1.0, 0.0, 1.0),
            ],
            ..Default::default()
        };
        tool.height = quantize_height(tool.height);

        let status = draw_brush_status(&tool);

        assert!(status.contains("4.2m"));
        assert!(status.contains("2 convex parts"));
    }

    #[test]
    fn brush_tool_cancel_resets_modal_state_without_authored_side_effects() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let mut tool = BrushToolState {
            active: true,
            phase: BrushToolPhase::Height,
            vertices: vec![Vec3::ZERO, Vec3::X, Vec3::Z],
            height: 7.5,
            height_anchor_pointer_y: Some(200.0),
            height_anchor_value: 7.5,
        };

        tool.cancel();
        set_brush_tool_status(
            &mut world.resource_mut::<ActiveOperator>(),
            OperatorPhase::Canceled,
            "Canceled",
        );

        harness.assert_canceled(&mut world);
        assert!(!tool.active);
        assert_eq!(tool.phase, BrushToolPhase::Outline);
        assert!(tool.vertices.is_empty());
        assert_eq!(tool.height, DEFAULT_BRUSH_HEIGHT);
        assert!(tool.height_anchor_pointer_y.is_none());
    }

    #[test]
    fn decomposed_brush_commit_is_one_undo_group() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<SelectedEntity>();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let snapshots = brush_snapshots_from_points(
            &[
                Vec3::new(-1.0, 0.0, -1.0),
                Vec3::new(1.0, 0.0, -1.0),
                Vec3::new(0.0, 0.0, 0.0),
                Vec3::new(1.0, 0.0, 1.0),
                Vec3::new(-1.0, 0.0, 1.0),
            ],
            2.0,
        )
        .unwrap();

        spawn_many_with_history(&mut world, snapshots);
        set_brush_tool_status(
            &mut world.resource_mut::<ActiveOperator>(),
            OperatorPhase::Committed,
            "Committed brush.draw (2 parts, 2.0m)",
        );

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