use bevy::camera::visibility::RenderLayers;
use bevy::input::mouse::{AccumulatedMouseMotion, MouseWheel};
use bevy::prelude::*;
use bevy::window::{CursorOptions, PrimaryWindow};
use bevy_egui::{EguiContexts, EguiGlobalSettings, PrimaryEguiContext};
use transform_gizmo_bevy::prelude::GizmoCamera;

use crate::infra::EditorOnly;
use crate::state::scene_tools_active;
use crate::ui::{egui_captures_keyboard, UiState};
use settings::ProjectSettings;

#[derive(Component)]
pub struct EditorCamera {
    pub fly_speed: f32,
    pub look_sensitivity: f32,
    pub pan_speed: f32,
}

impl Default for EditorCamera {
    fn default() -> Self {
        Self {
            fly_speed: 14.0,
            look_sensitivity: 0.003,
            pan_speed: 0.015,
        }
    }
}

pub struct EditorCameraPlugin;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CameraNavigationMode {
    Look,
    Pan,
}

#[derive(Resource, Default)]
struct EditorCameraInputState {
    mode: Option<CameraNavigationMode>,
    cursor_hidden_for_look: bool,
    cursor_visible_before_look: bool,
}

impl Plugin for EditorCameraPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<EditorCameraInputState>()
            .add_systems(Startup, spawn_editor_camera)
            .add_systems(
                Update,
                (sync_editor_camera_from_settings, update_editor_camera)
                    .chain()
                    .run_if(scene_tools_active),
            );
    }
}

fn sync_editor_camera_from_settings(
    settings: Res<ProjectSettings>,
    mut cameras: Query<&mut EditorCamera>,
) {
    if !settings.is_changed() {
        return;
    }
    let input = &settings.input;
    for mut camera in &mut cameras {
        camera.fly_speed = input.editor_fly_speed;
        camera.look_sensitivity = input.editor_look_sensitivity;
        camera.pan_speed = input.editor_pan_speed;
    }
}

fn spawn_editor_camera(
    mut commands: Commands,
    mut egui_global_settings: ResMut<EguiGlobalSettings>,
) {
    // Own the egui primary context explicitly. If bevy_egui auto-attaches it to
    // a 3D camera, `set_camera_viewport` then crops egui's own screen rect to the
    // shrinking viewport region, and egui_dock eventually lays out into a NaN
    // rect (panic: "rect is nan in advance_cursor_after_rect").
    egui_global_settings.auto_create_primary_context = false;

    commands.spawn((
        Name::new("Editor Camera"),
        EditorOnly,
        EditorCamera::default(),
        RenderLayers::default(),
        GizmoCamera,
        Camera3d::default(),
        Msaa::Off,
        Camera {
            clear_color: ClearColorConfig::Custom(Color::srgba(0.0, 0.0, 0.0, 1.0)),
            ..default()
        },
        Transform::from_xyz(10.0, 8.0, 18.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    // Dedicated full-window 2D camera that renders only egui on top of the 3D
    // scene. Keeping the egui context off the 3D cameras avoids the
    // viewport-cropping feedback loop described above.
    commands.spawn((
        Name::new("Editor Egui Camera"),
        EditorOnly,
        Camera2d,
        PrimaryEguiContext,
        RenderLayers::none(),
        Camera {
            order: 10,
            clear_color: ClearColorConfig::None,
            ..default()
        },
    ));
}

#[allow(clippy::too_many_arguments)]
fn update_editor_camera(
    time: Res<Time>,
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mouse_motion: Res<AccumulatedMouseMotion>,
    mut scroll: MessageReader<MouseWheel>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    mut input_state: ResMut<EditorCameraInputState>,
    mut cameras: Query<(&mut Transform, &EditorCamera)>,
    mut cursor: Single<&mut CursorOptions, With<PrimaryWindow>>,
) -> Result {
    let ctx = contexts.ctx_mut()?;
    let in_scene = ui_state.pointer_in_viewport;
    let navigation_pressed =
        buttons.pressed(MouseButton::Right) || buttons.pressed(MouseButton::Middle);
    if !navigation_pressed {
        input_state.mode = None;
        restore_editor_camera_cursor(&mut input_state, &mut cursor);
    }
    if egui_captures_keyboard(ctx) {
        input_state.mode = None;
        restore_editor_camera_cursor(&mut input_state, &mut cursor);
        return Ok(());
    }
    if in_scene && buttons.just_pressed(MouseButton::Right) {
        input_state.mode = Some(CameraNavigationMode::Look);
        input_state.cursor_visible_before_look = cursor.visible;
        input_state.cursor_hidden_for_look = true;
        cursor.visible = false;
    } else if in_scene && buttons.just_pressed(MouseButton::Middle) {
        input_state.mode = Some(CameraNavigationMode::Pan);
        restore_editor_camera_cursor(&mut input_state, &mut cursor);
    }
    if input_state.mode.is_none() && navigation_pressed {
        return Ok(());
    }
    if !in_scene && input_state.mode.is_none() && ctx.egui_wants_pointer_input() {
        return Ok(());
    }
    if in_scene
        && input_state.mode.is_none()
        && ctx.egui_wants_pointer_input()
        && !buttons.pressed(MouseButton::Right)
    {
        return Ok(());
    }

    let dt = time.delta_secs();
    let raw_scroll_delta: f32 = scroll.read().map(|event| event.y).sum();
    let scroll_delta = if in_scene && !ctx.egui_wants_pointer_input() {
        raw_scroll_delta
    } else {
        0.0
    };

    for (mut transform, camera) in &mut cameras {
        let looking = matches!(input_state.mode, Some(CameraNavigationMode::Look))
            && buttons.pressed(MouseButton::Right);
        let panning = matches!(input_state.mode, Some(CameraNavigationMode::Pan))
            && buttons.pressed(MouseButton::Middle);

        if looking {
            let delta = mouse_motion.delta;
            if delta != Vec2::ZERO {
                let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
                let yaw = yaw - delta.x * camera.look_sensitivity;
                let pitch = (pitch - delta.y * camera.look_sensitivity).clamp(
                    -std::f32::consts::FRAC_PI_2 + 0.01,
                    std::f32::consts::FRAC_PI_2 - 0.01,
                );
                transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
            }
        }

        let forward = transform.forward().as_vec3();
        let right = transform.right().as_vec3();
        let up = Vec3::Y;
        let mut wish = Vec3::ZERO;

        if looking {
            if keys.pressed(KeyCode::KeyW) {
                wish += forward;
            }
            if keys.pressed(KeyCode::KeyS) {
                wish -= forward;
            }
            if keys.pressed(KeyCode::KeyD) {
                wish += right;
            }
            if keys.pressed(KeyCode::KeyA) {
                wish -= right;
            }
            if keys.pressed(KeyCode::KeyE) {
                wish += up;
            }
            if keys.pressed(KeyCode::KeyQ) {
                wish -= up;
            }
        }

        let sprint = if keys.pressed(KeyCode::ShiftLeft) {
            3.0
        } else {
            1.0
        };
        transform.translation += wish.normalize_or_zero() * camera.fly_speed * sprint * dt;
        transform.translation += forward * scroll_delta * camera.fly_speed * 0.08;

        if panning {
            let delta = mouse_motion.delta;
            let distance_scale = transform.translation.length().max(1.0).sqrt();
            transform.translation +=
                (-right * delta.x + up * delta.y) * camera.pan_speed * distance_scale;
        }
    }

    Ok(())
}

fn restore_editor_camera_cursor(
    input_state: &mut EditorCameraInputState,
    cursor: &mut CursorOptions,
) {
    if input_state.cursor_hidden_for_look {
        cursor.visible = input_state.cursor_visible_before_look;
        input_state.cursor_hidden_for_look = false;
    }
}
