use bevy::ecs::system::SystemParam;
use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility};
use bevy::picking::pointer::{PointerAction, PointerInput, PointerInteraction};
use bevy::prelude::*;
use shared::{HydratedPrefabMember, LevelObject};
use transform_gizmo_bevy::prelude::GizmoTarget;

use crate::camera::EditorCamera;
use crate::infra::EditorOnly;
use crate::scene_io::ComposedSceneMember;
use crate::state::scene_tools_active;
use crate::state::EditorMode;
use crate::ui::helpers::ensure_player_spawn_for_edit;
use crate::ui::hierarchy_ops::is_entity_locked;
use crate::ui::hierarchy_state::HierarchyPanelState;
use crate::ui::UiState;
use crate::viewport::actor_icons::ActorIconProxy;
use crate::viewport::brush_edit::{BrushEditMode, BrushElementGizmo};
use crate::viewport::brush_tool::BrushToolState;
use crate::viewport::ViewportDisplayMode;
use crate::visualizers::EditorVisualizerProxy;
use bevy_egui::egui;

use crate::viewport::material_drop::{MaterialDropSet, MaterialDropState};
use crate::viewport::scene_view_ray;

#[derive(Resource, Default, Debug, Clone, Copy)]
pub struct SelectedEntity(pub Option<Entity>);

/// Primary click on the viewport panel (egui), consumed by [`handle_pick_events`].
#[derive(Resource, Default, Debug, Clone, Copy)]
pub struct ViewportClick(pub Option<egui::Pos2>);

/// Overlapping pick targets at the last viewport click; **Tab** cycles forward.
#[derive(Resource, Default, Debug)]
pub struct ViewportPickStack {
    pub entities: Vec<Entity>,
    pub index: usize,
    pub anchor: Option<egui::Pos2>,
}

pub struct EditorSelectionPlugin;

#[derive(SystemParam)]
struct PickTargetQueries<'w, 's> {
    level_objects: Query<'w, 's, Entity, (With<LevelObject>, Without<EditorOnly>)>,
    proxies: Query<'w, 's, &'static EditorVisualizerProxy>,
    icon_proxies: Query<'w, 's, &'static ActorIconProxy>,
    parents: Query<'w, 's, &'static ChildOf>,
    editor_only: Query<'w, 's, (), With<EditorOnly>>,
    material_drop: Option<Res<'w, MaterialDropState>>,
}

impl Plugin for EditorSelectionPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<SelectedEntity>()
            .init_resource::<ViewportClick>()
            .init_resource::<ViewportPickStack>()
            .add_systems(
                Update,
                (
                    redirect_runtime_player_selection_to_spawn,
                    sync_gizmo_targets,
                )
                    .chain()
                    .run_if(scene_tools_active),
            )
            // After transform-gizmo `update_gizmos` (Last) so clicks hit gizmos first.
            .add_systems(
                Last,
                (handle_pick_events, cycle_overlapping_viewport_pick)
                    .chain()
                    .after(MaterialDropSet)
                    .run_if(scene_tools_active),
            );
    }
}

fn redirect_runtime_player_selection_to_spawn(world: &mut World) {
    if *world.resource::<State<EditorMode>>().get() != EditorMode::Editing {
        return;
    }
    let Some(selected_entity) = world.resource::<SelectedEntity>().0 else {
        return;
    };
    if world.get::<sim::Player>(selected_entity).is_none() {
        return;
    }

    let spawn = ensure_player_spawn_for_edit(world);
    world.resource_mut::<SelectedEntity>().0 = Some(spawn);
    if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
        ui_state.selected_entities.select_replace(spawn);
    }
}

#[allow(clippy::too_many_arguments)]
fn handle_pick_events(
    mut ui_state: ResMut<UiState>,
    mut selected: ResMut<SelectedEntity>,
    mut viewport_click: ResMut<ViewportClick>,
    mut pick_stack: ResMut<ViewportPickStack>,
    mut click_events: MessageReader<PointerInput>,
    pointers: Query<&PointerInteraction>,
    editor_cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    mut mesh_ray_cast: MeshRayCast,
    pick_targets: PickTargetQueries,
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    display: Res<ViewportDisplayMode>,
    brush_mode: Res<BrushEditMode>,
    brush_tool: Res<BrushToolState>,
    gizmo_targets: Query<&GizmoTarget>,
    hierarchy: Option<Res<HierarchyPanelState>>,
) -> Result {
    if pick_targets
        .material_drop
        .as_deref()
        .is_some_and(MaterialDropState::captures_viewport_input)
    {
        viewport_click.0 = None;
        return Ok(());
    }
    if ui_state.pointer_in_viewport
        && !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
        && keys.just_pressed(KeyCode::Escape)
    {
        ui_state.selected_entities.clear();
        selected.0 = None;
        pick_stack.entities.clear();
        pick_stack.index = 0;
        pick_stack.anchor = None;
        return Ok(());
    }

    if display.clean_game_view {
        viewport_click.0 = None;
        return Ok(());
    }

    if brush_tool.active || brush_mode.is_element_mode() {
        return Ok(());
    }

    if gizmo_targets
        .iter()
        .any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
    {
        viewport_click.0 = None;
        return Ok(());
    }

    let additive = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]);

    if let Some(pointer_pos) = viewport_click.0.take() {
        apply_viewport_pick(
            hierarchy.as_deref(),
            &mut ui_state,
            &mut selected,
            &mut pick_stack,
            &editor_cameras,
            &mut mesh_ray_cast,
            &pick_targets,
            pointer_pos,
            additive,
        );
        return Ok(());
    }

    if !ui_state.pointer_in_viewport {
        return Ok(());
    }

    for event in click_events.read() {
        if !matches!(event.action, PointerAction::Press(PointerButton::Primary)) {
            continue;
        }

        let mut picked = None;
        for interaction in &pointers {
            if let Some(entity) = interaction
                .as_slice()
                .iter()
                .find_map(|(entity, _hit)| pick_target_for_entity(*entity, &pick_targets))
            {
                picked = Some(entity);
                break;
            }
        }

        if picked.is_none() {
            let Some(pointer_pos) = ui_state.viewport_pointer_pos else {
                continue;
            };
            let hits = collect_viewport_pick_hits(
                &editor_cameras,
                &mut mesh_ray_cast,
                &pick_targets,
                ui_state.viewport_rect,
                pointer_pos,
            );
            pick_stack.entities = hits;
            pick_stack.index = 0;
            pick_stack.anchor = Some(pointer_pos);
            picked = pick_stack.entities.first().copied();
        } else {
            pick_stack.entities.clear();
            pick_stack.index = 0;
            pick_stack.anchor = ui_state.viewport_pointer_pos;
        }

        apply_pick_result(
            hierarchy.as_deref(),
            &mut ui_state,
            &mut selected,
            picked,
            additive,
        );
    }
    Ok(())
}

fn cycle_overlapping_viewport_pick(
    mut ui_state: ResMut<UiState>,
    mut selected: ResMut<SelectedEntity>,
    mut pick_stack: ResMut<ViewportPickStack>,
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    display: Res<ViewportDisplayMode>,
    material_drop: Option<Res<MaterialDropState>>,
) -> Result {
    if material_drop
        .as_deref()
        .is_some_and(MaterialDropState::captures_viewport_input)
    {
        return Ok(());
    }
    if display.clean_game_view {
        return Ok(());
    }
    if !ui_state.pointer_in_viewport
        || buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
    {
        return Ok(());
    }
    if !keys.just_pressed(KeyCode::Tab) || pick_stack.entities.len() < 2 {
        return Ok(());
    }
    if keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]) {
        return Ok(());
    }

    pick_stack.index = (pick_stack.index + 1) % pick_stack.entities.len();
    let entity = pick_stack.entities[pick_stack.index];
    ui_state.selected_entities.select_replace(entity);
    selected.0 = Some(entity);
    Ok(())
}

#[expect(
    clippy::too_many_arguments,
    reason = "viewport picking combines explicit camera, ray-cast, selection, and input state"
)]
fn apply_viewport_pick(
    hierarchy: Option<&HierarchyPanelState>,
    ui_state: &mut UiState,
    selected: &mut SelectedEntity,
    pick_stack: &mut ViewportPickStack,
    editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    mesh_ray_cast: &mut MeshRayCast,
    pick_targets: &PickTargetQueries,
    pointer_pos: egui::Pos2,
    additive: bool,
) {
    let hits = collect_viewport_pick_hits(
        editor_cameras,
        mesh_ray_cast,
        pick_targets,
        ui_state.viewport_rect,
        pointer_pos,
    );
    pick_stack.entities = hits;
    pick_stack.index = 0;
    pick_stack.anchor = Some(pointer_pos);
    let picked = pick_stack.entities.first().copied();
    apply_pick_result(hierarchy, ui_state, selected, picked, additive);
}

fn apply_pick_result(
    hierarchy: Option<&HierarchyPanelState>,
    ui_state: &mut UiState,
    selected: &mut SelectedEntity,
    picked: Option<Entity>,
    additive: bool,
) {
    if let Some(entity) = picked {
        if hierarchy.is_some_and(|state| is_entity_locked(&state.locked, entity)) {
            return;
        }
        ui_state
            .selected_entities
            .select_maybe_add(entity, additive);
        selected.0 = Some(entity);
    } else if !additive {
        ui_state.selected_entities.clear();
        selected.0 = None;
    }
}

fn collect_viewport_pick_hits(
    editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    mesh_ray_cast: &mut MeshRayCast,
    pick_targets: &PickTargetQueries,
    scene_rect: egui::Rect,
    pointer_pos: egui::Pos2,
) -> Vec<Entity> {
    if !scene_rect.contains(pointer_pos) {
        return Vec::new();
    }

    let (camera, transform) = match editor_cameras
        .iter()
        .find(|(camera, _)| camera.is_active)
        .or_else(|| editor_cameras.iter().next())
    {
        Some(pair) => pair,
        None => return Vec::new(),
    };
    let Some(ray) = scene_view_ray(camera, transform, pointer_pos, scene_rect) else {
        return Vec::new();
    };
    let filter = |entity: Entity| pick_target_for_entity(entity, pick_targets).is_some();
    let settings = MeshRayCastSettings::default()
        .with_visibility(RayCastVisibility::Any)
        .with_filter(&filter)
        .never_early_exit();
    let hits = mesh_ray_cast.cast_ray(ray, &settings);

    let mut seen = std::collections::HashSet::new();
    let mut ordered = Vec::new();
    for icon_priority in [true, false] {
        for (entity, _) in hits {
            let is_icon = pick_targets.icon_proxies.get(*entity).is_ok();
            if is_icon != icon_priority {
                continue;
            }
            let Some(target) = pick_target_for_entity(*entity, pick_targets) else {
                continue;
            };
            if seen.insert(target) {
                ordered.push(target);
            }
        }
    }
    ordered
}

fn pick_target_for_entity(entity: Entity, pick_targets: &PickTargetQueries) -> Option<Entity> {
    if let Ok(proxy) = pick_targets.icon_proxies.get(entity) {
        return pick_targets
            .level_objects
            .contains(proxy.target)
            .then_some(proxy.target);
    }
    if let Ok(proxy) = pick_targets.proxies.get(entity) {
        return Some(proxy.target);
    }
    if pick_targets.editor_only.contains(entity) {
        return None;
    }
    if pick_targets.level_objects.contains(entity) {
        return Some(entity);
    }
    let mut current = pick_targets
        .parents
        .get(entity)
        .ok()
        .map(|parent| parent.parent());
    while let Some(parent) = current {
        if pick_targets.editor_only.contains(parent) {
            return None;
        }
        if pick_targets.level_objects.contains(parent) {
            return Some(parent);
        }
        current = pick_targets
            .parents
            .get(parent)
            .ok()
            .map(|parent| parent.parent());
    }
    None
}

#[expect(
    clippy::too_many_arguments,
    reason = "Bevy system parameters represent independent selection and gizmo state"
)]
fn sync_gizmo_targets(
    mut ui_state: ResMut<UiState>,
    mut selected: ResMut<SelectedEntity>,
    mut commands: Commands,
    targets: Query<(Entity, Option<&BrushElementGizmo>), With<GizmoTarget>>,
    level_objects: Query<
        (),
        (
            With<LevelObject>,
            Without<EditorOnly>,
            Without<HydratedPrefabMember>,
            Without<ComposedSceneMember>,
        ),
    >,
    editor_only: Query<(), With<EditorOnly>>,
    transforms: Query<(), With<Transform>>,
    hierarchy: Option<Res<HierarchyPanelState>>,
    display: Res<ViewportDisplayMode>,
    brush_mode: Res<BrushEditMode>,
) {
    ui_state
        .selected_entities
        .retain(|entity| transforms.contains(entity) && !editor_only.contains(entity));
    selected.0 = ui_state.selected_entities.as_slice().first().copied();

    if display.clean_game_view || brush_mode.is_element_mode() {
        for (entity, brush_element_gizmo) in &targets {
            if brush_element_gizmo.is_none() {
                commands.entity(entity).remove::<GizmoTarget>();
            }
        }
        return;
    }

    for (entity, brush_element_gizmo) in &targets {
        if brush_element_gizmo.is_some() {
            continue;
        }
        if !ui_state.selected_entities.contains(entity) || !level_objects.contains(entity) {
            commands.entity(entity).remove::<GizmoTarget>();
        }
    }
    for entity in ui_state.selected_entities.iter() {
        if level_objects.contains(entity)
            && transforms.contains(entity)
            && !targets.contains(entity)
            && !hierarchy
                .as_ref()
                .is_some_and(|state| is_entity_locked(&state.locked, entity))
        {
            commands.entity(entity).insert(GizmoTarget::default());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn multi_selection_receives_grouped_gizmo_targets() {
        let mut app = App::new();
        let first = app
            .world_mut()
            .spawn((LevelObject, Transform::default()))
            .id();
        let second = app
            .world_mut()
            .spawn((LevelObject, Transform::default()))
            .id();

        let mut ui_state = UiState::default_layout();
        ui_state.selected_entities.select_replace(first);
        ui_state.selected_entities.select_maybe_add(second, true);
        app.insert_resource(ui_state)
            .init_resource::<SelectedEntity>()
            .init_resource::<ViewportDisplayMode>()
            .init_resource::<BrushEditMode>()
            .add_systems(Update, sync_gizmo_targets);

        app.update();
        assert!(app.world().get::<GizmoTarget>(first).is_some());
        assert!(app.world().get::<GizmoTarget>(second).is_some());

        app.world_mut()
            .resource_mut::<UiState>()
            .selected_entities
            .select_replace(first);
        app.update();
        assert!(app.world().get::<GizmoTarget>(first).is_some());
        assert!(app.world().get::<GizmoTarget>(second).is_none());
    }
}
