//! Shared viewport controller: rects, raycast placement, grid, focus, bookmarks.

use crate::camera::EditorCamera;
use crate::selection::SelectedEntity;
use crate::state::scene_tools_active;
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
use crate::viewport::viewport_mode::EditorViewportModePlugin;
use bevy::prelude::*;
use bevy::window::Window;
use bevy_egui::{egui, EguiContexts};

/// Viewport display and snapping options (editor-only).
#[derive(Resource, Debug, Clone)]
pub struct ViewportSettings {
    pub show_grid: bool,
    pub snap_translate: bool,
    pub snap_increment: f32,
}

impl Default for ViewportSettings {
    fn default() -> Self {
        Self {
            show_grid: true,
            snap_translate: true,
            snap_increment: 1.0,
        }
    }
}

/// Primary viewport display state. `clean_game_view` hides editor-only overlays without
/// changing the active camera or play possession.
#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct ViewportDisplayMode {
    pub clean_game_view: bool,
}

/// Saved editor fly-camera poses keyed by scene path.
#[derive(Resource, Debug, Default)]
pub struct CameraBookmarks {
    pub poses: Vec<(String, Transform)>,
}

pub struct ViewportPlugin;

impl Plugin for ViewportPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<ViewportSettings>()
            .init_resource::<ViewportDisplayMode>()
            .init_resource::<CameraBookmarks>()
            .add_plugins(EditorViewportModePlugin)
            .add_systems(
                Update,
                (
                    focus_selection_hotkey,
                    toggle_viewport_hotkeys,
                    camera_bookmark_hotkeys,
                    draw_viewport_grid,
                )
                    .run_if(scene_tools_active),
            );
    }
}

/// Physical pixel size of a dock panel rect (for render-to-texture targets).
pub fn panel_physical_size(window: &Window, rect: egui::Rect) -> Option<UVec2> {
    if rect == egui::Rect::NOTHING || rect.width() <= 1.0 || rect.height() <= 1.0 {
        return None;
    }

    let scale = window.scale_factor();
    let size = rect.size() * scale;
    Some(UVec2::new(size.x.max(64.0) as u32, size.y.max(64.0) as u32))
}

/// Builds a world-space ray from a viewport pointer position (render-to-texture camera).
///
/// Maps panel-local egui coordinates into the editor camera's [`Camera::logical_viewport_rect`]
/// so picking matches the offscreen target rather than the primary window.
pub fn scene_view_ray(
    camera: &Camera,
    transform: &GlobalTransform,
    pointer_pos: egui::Pos2,
    scene_rect: egui::Rect,
) -> Option<Ray3d> {
    if !scene_rect.contains(pointer_pos) {
        return None;
    }
    let rel = pointer_pos - scene_rect.min;
    let panel_size = scene_rect.size();
    if panel_size.x <= f32::EPSILON || panel_size.y <= f32::EPSILON {
        return None;
    }
    let rel_uv = Vec2::new(rel.x / panel_size.x, rel.y / panel_size.y);

    if let Some(viewport) = camera.logical_viewport_rect() {
        let logical_pos =
            viewport.min + Vec2::new(rel_uv.x * viewport.width(), rel_uv.y * viewport.height());
        return camera.viewport_to_world(transform, logical_pos).ok();
    }

    // Before `camera_system` fills target_info, build a ray from panel UV via NDC.
    let mut ndc = rel_uv * 2.0 - Vec2::ONE;
    ndc.y = -ndc.y;
    let view_from_clip = camera.computed.clip_from_view.inverse();
    let world_from_view = transform.affine();
    let view_near = view_from_clip.project_point3a(ndc.extend(1.0).into());
    let view_far = view_from_clip.project_point3a(ndc.extend(f32::EPSILON).into());
    let view_dir = view_far - view_near;
    let origin = world_from_view.transform_point3a(view_near).into();
    let direction = world_from_view.transform_vector3a(view_dir).into();
    Dir3::new(direction)
        .ok()
        .map(|direction| Ray3d { origin, direction })
}

/// Spawns placement position from editor camera center ray to ground plane.
pub fn editor_spawn_ground_position(world: &mut World, scene_rect: egui::Rect) -> Vec3 {
    let center = scene_rect.center();
    viewport_ground_position(world, center, scene_rect).unwrap_or(Vec3::ZERO)
}

/// Raycasts from a viewport pointer position to the ground plane (y = 0).
pub fn viewport_ground_position(
    world: &mut World,
    pointer_pos: egui::Pos2,
    scene_rect: egui::Rect,
) -> Option<Vec3> {
    let mut query = world.query_filtered::<(&Camera, &GlobalTransform), With<EditorCamera>>();
    for (camera, transform) in query.iter(world) {
        if !camera.is_active {
            continue;
        }
        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;
        return Some(ray.origin + direction * t.max(0.0));
    }
    None
}

/// Snaps a world position to the configured grid increment.
pub fn snap_translation(position: Vec3, settings: &ViewportSettings) -> Vec3 {
    if !settings.snap_translate {
        return position;
    }
    let step = settings.snap_increment.max(0.01);
    Vec3::new(
        (position.x / step).round() * step,
        position.y,
        (position.z / step).round() * step,
    )
}

#[expect(
    clippy::too_many_arguments,
    reason = "Bevy system parameters represent independent editor input and scene state"
)]
fn focus_selection_hotkey(
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    display: Res<ViewportDisplayMode>,
    selected: Res<SelectedEntity>,
    mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
    targets: Query<&GlobalTransform>,
) -> Result {
    if display.clean_game_view {
        return Ok(());
    }
    let ctx = contexts.ctx_mut()?;
    if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
        return Ok(());
    }
    if !keys.just_pressed(KeyCode::KeyF) {
        return Ok(());
    }
    let Some(entity) = selected.0 else {
        return Ok(());
    };
    let Ok(target) = targets.get(entity) else {
        return Ok(());
    };
    let center = target.translation();
    for mut transform in &mut editor_cameras {
        let offset = transform.translation - center;
        let distance = offset.length().max(8.0);
        transform.translation = center - transform.forward().as_vec3() * distance;
        transform.look_at(center, Vec3::Y);
    }
    Ok(())
}

fn toggle_viewport_hotkeys(
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    mut settings: ResMut<ViewportSettings>,
    mut display: ResMut<ViewportDisplayMode>,
) -> Result {
    let ctx = contexts.ctx_mut()?;
    if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
        return Ok(());
    }
    if keys.just_pressed(KeyCode::KeyG) {
        let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
        if ctrl {
            settings.show_grid = !settings.show_grid;
        } else {
            display.clean_game_view = !display.clean_game_view;
        }
    }
    Ok(())
}

fn camera_bookmark_hotkeys(
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    scene_io: Res<crate::scene_io::SceneIo>,
    mut bookmarks: ResMut<CameraBookmarks>,
    mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
) -> Result {
    let ctx = contexts.ctx_mut()?;
    if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
        return Ok(());
    }
    let shift = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
    let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
    if !shift || !ctrl {
        return Ok(());
    }

    let scene_key = scene_io
        .active_path
        .as_ref()
        .map(|path| path.display().to_string())
        .unwrap_or_else(|| "__unsaved__".into());

    if keys.just_pressed(KeyCode::Digit1) {
        let Ok(transform) = editor_cameras.single() else {
            return Ok(());
        };
        save_camera_bookmark(&mut bookmarks, &scene_key, *transform);
    }

    if keys.just_pressed(KeyCode::Digit2) {
        let Some(saved) = recall_camera_bookmark(&bookmarks, &scene_key) else {
            return Ok(());
        };
        for mut transform in &mut editor_cameras {
            *transform = saved;
        }
    }
    Ok(())
}

fn draw_viewport_grid(
    settings: Res<ViewportSettings>,
    display: Res<ViewportDisplayMode>,
    mut gizmos: Gizmos,
) {
    if display.clean_game_view || !settings.show_grid {
        return;
    }

    let step = settings.snap_increment.max(0.25);
    let extent = 40.0;
    let divisions = (extent / step).ceil() as i32;
    for index in -divisions..=divisions {
        let coordinate = index as f32 * step;
        if coordinate.abs() > extent || index == 0 {
            continue;
        }
        let major = index.rem_euclid(10) == 0;
        let color = if major {
            Color::srgba(0.43, 0.46, 0.5, 0.38)
        } else {
            Color::srgba(0.36, 0.39, 0.43, 0.2)
        };
        gizmos.line(
            Vec3::new(coordinate, 0.0, -extent),
            Vec3::new(coordinate, 0.0, extent),
            color,
        );
        gizmos.line(
            Vec3::new(-extent, 0.0, coordinate),
            Vec3::new(extent, 0.0, coordinate),
            color,
        );
    }

    gizmos.line(
        Vec3::new(-extent, 0.002, 0.0),
        Vec3::new(extent, 0.002, 0.0),
        Color::srgba(0.91, 0.33, 0.41, 0.72),
    );
    gizmos.line(
        Vec3::new(0.0, 0.002, -extent),
        Vec3::new(0.0, 0.002, extent),
        Color::srgba(0.3, 0.58, 0.92, 0.72),
    );
}

/// Stores the current editor camera as a bookmark named by scene path.
pub fn save_camera_bookmark(
    bookmarks: &mut CameraBookmarks,
    scene_path: &str,
    transform: Transform,
) {
    bookmarks.poses.retain(|(path, _)| path != scene_path);
    bookmarks.poses.push((scene_path.to_string(), transform));
}

/// Recalls a bookmark for the given scene path, if any.
pub fn recall_camera_bookmark(bookmarks: &CameraBookmarks, scene_path: &str) -> Option<Transform> {
    bookmarks
        .poses
        .iter()
        .find(|(path, _)| path == scene_path)
        .map(|(_, transform)| *transform)
}
