//! Blender-style navigation gizmo overlay.
//!
//! Renders six colored axis-end discs (+X / -X, +Y / -Y, +Z / -Z) pinned to
//! the top-right of the active viewport tile. Three modes of interaction:
//!
//! 1. **Click a disc**: snap the active camera to look along that axis.
//!    For pan-orbit and third-person cameras the snap targets
//!    `target_yaw` / `target_pitch` so the existing interpolation produces
//!    smooth motion. Fly cameras are reoriented instantly around a focal
//!    point picked at `FLY_FOCAL_DISTANCE` along the current forward.
//! 2. **Click-and-drag inside the gizmo region**: orbit the active camera
//!    using the same math as `pan_orbit_mouse_input`'s orbit branch. If
//!    the mouse moves more than `DRAG_THRESHOLD_PX` between press and
//!    release, no snap is performed.
//! 3. **Hover**: claims `hud_wants_pointer` so other systems (camera
//!    controllers, picking) skip the mouse on the next frame.
//!
//! All geometry is pushed through `world.resources.retained_ui.frame.rects`
//! and `overlay_text`, so the gizmo rides the same GPU-driven UI passes the
//! rest of the retained UI uses. Only `Gizmos.nav_gizmo_drag` persists
//! across frames; everything else is recomputed each frame from the camera
//! basis vectors and viewport rect.

use super::overlays_enabled::active_camera_overlays_enabled;
use super::state::{NavGizmoDisc, NavGizmoDrag};
use crate::ecs::input::resources::MouseState;
use crate::ecs::text::components::{TextAlignment, TextProperties, VerticalAlignment};
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::world::{Entity, PAN_ORBIT_CAMERA, THIRD_PERSON_CAMERA, World};
use crate::render::wgpu::passes::geometry::{UiLayer, UiRect};
use nalgebra_glm::{Mat3, Quat, Vec2, Vec3, Vec4};

const Z_INDEX_BASE: i32 = 6_000;
const ANCHOR_INSET_PX: f32 = 56.0;
const RING_RADIUS_PX: f32 = 32.0;
const DISC_RADIUS_PX: f32 = 11.0;
const HOVER_DISC_RADIUS_PX: f32 = 13.0;
const CONNECTOR_THICKNESS_PX: f32 = 2.0;
const NEGATIVE_BORDER_PX: f32 = 1.5;
const GIZMO_REGION_RADIUS_PX: f32 = RING_RADIUS_PX + HOVER_DISC_RADIUS_PX + 4.0;
const DRAG_THRESHOLD_PX: f32 = 4.0;
const LABEL_FONT_SIZE_PX: f32 = 12.0;
const FLY_FOCAL_DISTANCE: f32 = 10.0;

const X_BRIGHT: Vec4 = Vec4::new(0.95, 0.30, 0.30, 1.0);
const Y_BRIGHT: Vec4 = Vec4::new(0.40, 0.95, 0.30, 1.0);
const Z_BRIGHT: Vec4 = Vec4::new(0.30, 0.55, 0.95, 1.0);
const X_DIM: Vec4 = Vec4::new(0.55, 0.20, 0.20, 0.85);
const Y_DIM: Vec4 = Vec4::new(0.25, 0.55, 0.20, 0.85);
const Z_DIM: Vec4 = Vec4::new(0.20, 0.32, 0.55, 0.85);
const HOVER_COLOR: Vec4 = Vec4::new(1.0, 0.85, 0.20, 1.0);
const NEGATIVE_FILL: Vec4 = Vec4::new(0.0, 0.0, 0.0, 0.45);
const LABEL_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 1.0);
const CONNECTOR_DIM: f32 = 0.55;

const AXES: [(Vec3, &str, Vec4, Vec4); 3] = [
    (Vec3::new(1.0, 0.0, 0.0), "X", X_BRIGHT, X_DIM),
    (Vec3::new(0.0, 1.0, 0.0), "Y", Y_BRIGHT, Y_DIM),
    (Vec3::new(0.0, 0.0, 1.0), "Z", Z_BRIGHT, Z_DIM),
];

#[derive(Clone, Copy)]
struct DiscScreen {
    center: Vec2,
    depth: f32,
    is_positive: bool,
    axis_index: u8,
    color: Vec4,
    label: &'static str,
}

pub fn nav_gizmo_overlay_system(world: &mut World) {
    if !world.resources.retained_ui.enabled
        || !world.resources.user_interface.gizmos.nav_gizmo_enabled
        || !active_camera_overlays_enabled(world)
    {
        world.resources.user_interface.gizmos.nav_gizmo_drag = None;
        return;
    }

    let Some(camera_entity) = world.resources.active_camera else {
        world.resources.user_interface.gizmos.nav_gizmo_drag = None;
        return;
    };

    let viewport = match world
        .resources
        .window
        .camera_tile_rects
        .get(&camera_entity)
        .copied()
        .or(world.resources.window.active_viewport_rect)
    {
        Some(rect) if rect.width > 1.0 && rect.height > 1.0 => rect,
        _ => {
            world.resources.user_interface.gizmos.nav_gizmo_drag = None;
            return;
        }
    };

    let Some(global_transform) = world.core.get_global_transform(camera_entity) else {
        return;
    };
    let camera_right = global_transform.right_vector();
    let camera_up = global_transform.up_vector();
    let camera_forward = global_transform.forward_vector();

    let anchor = Vec2::new(
        viewport.x + viewport.width - ANCHOR_INSET_PX,
        viewport.y + ANCHOR_INSET_PX,
    );

    let mut discs = build_discs(anchor, &camera_right, &camera_up, &camera_forward);
    discs.sort_by(|a, b| {
        b.depth
            .partial_cmp(&a.depth)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let clip = Some(crate::ecs::ui::types::Rect::new(
        viewport.x,
        viewport.y,
        viewport.width,
        viewport.height,
    ));

    let mouse_pos = crate::ecs::input::access::mouse_for_active(world).position;
    let mouse_delta = crate::ecs::input::access::mouse_for_active(world).position_delta;
    let mouse_in_viewport = viewport.contains(mouse_pos);
    let just_pressed = world
        .resources
        .input
        .mouse
        .state
        .contains(MouseState::LEFT_JUST_PRESSED);
    let just_released = world
        .resources
        .input
        .mouse
        .state
        .contains(MouseState::LEFT_JUST_RELEASED);
    let held = world
        .resources
        .input
        .mouse
        .state
        .contains(MouseState::LEFT_CLICKED);

    let hovered_index = if mouse_in_viewport {
        nearest_disc(&discs, mouse_pos)
    } else {
        None
    };

    let mut drag_state = world.resources.user_interface.gizmos.nav_gizmo_drag;

    if just_pressed && mouse_in_viewport && distance(mouse_pos, anchor) <= GIZMO_REGION_RADIUS_PX {
        drag_state = Some(NavGizmoDrag {
            press_position: mouse_pos,
            pressed_disc: hovered_index.map(|index| NavGizmoDisc {
                axis_index: discs[index].axis_index,
                is_positive: discs[index].is_positive,
            }),
            did_drag: false,
        });
    }

    if held && let Some(drag) = drag_state.as_mut() {
        let press_distance = (mouse_pos - drag.press_position).magnitude();
        if !drag.did_drag && press_distance > DRAG_THRESHOLD_PX {
            drag.did_drag = true;
        }
        if drag.did_drag && (mouse_delta.x.abs() > 0.001 || mouse_delta.y.abs() > 0.001) {
            apply_orbit_drag(
                world,
                camera_entity,
                viewport.width,
                viewport.height,
                mouse_delta,
            );
        }
    }

    if just_released
        && let Some(drag) = drag_state.take()
        && !drag.did_drag
        && let Some(disc) = drag.pressed_disc
    {
        snap_camera(world, camera_entity, disc.axis_index, disc.is_positive);
    }

    if !held {
        drag_state = None;
    }

    push_connectors(world, anchor, &discs, clip);
    for (sorted_index, disc) in discs.iter().enumerate() {
        push_disc(
            world,
            disc,
            hovered_index == Some(sorted_index),
            sorted_index,
            clip,
        );
    }
    for disc in discs.iter() {
        if disc.is_positive {
            push_label(world, disc, clip);
        }
    }

    if hovered_index.is_some() || drag_state.is_some() {
        world.resources.user_interface.hud_wants_pointer = true;
    }

    world.resources.user_interface.gizmos.nav_gizmo_drag = drag_state;
}

fn distance(a: Vec2, b: Vec2) -> f32 {
    let delta = a - b;
    (delta.x * delta.x + delta.y * delta.y).sqrt()
}

fn build_discs(anchor: Vec2, right: &Vec3, up: &Vec3, forward: &Vec3) -> [DiscScreen; 6] {
    let mut discs = [DiscScreen {
        center: Vec2::zeros(),
        depth: 0.0,
        is_positive: true,
        axis_index: 0,
        color: Vec4::zeros(),
        label: "",
    }; 6];

    let mut slot = 0;
    for (axis_index, (axis_world, label, bright, dim)) in AXES.iter().copied().enumerate() {
        for (sign, is_positive) in [(1.0_f32, true), (-1.0_f32, false)] {
            let direction = axis_world * sign;
            let right_proj = nalgebra_glm::dot(&direction, right);
            let up_proj = nalgebra_glm::dot(&direction, up);
            let forward_proj = nalgebra_glm::dot(&direction, forward);
            discs[slot] = DiscScreen {
                center: anchor + Vec2::new(right_proj, -up_proj) * RING_RADIUS_PX,
                depth: forward_proj,
                is_positive,
                axis_index: axis_index as u8,
                color: if is_positive { bright } else { dim },
                label,
            };
            slot += 1;
        }
    }
    discs
}

fn nearest_disc(discs: &[DiscScreen; 6], mouse_pos: Vec2) -> Option<usize> {
    let radius_squared = HOVER_DISC_RADIUS_PX * HOVER_DISC_RADIUS_PX;
    let mut best: Option<(usize, f32)> = None;
    for (index, disc) in discs.iter().enumerate() {
        let to_mouse = mouse_pos - disc.center;
        let distance_squared = to_mouse.x * to_mouse.x + to_mouse.y * to_mouse.y;
        if distance_squared > radius_squared {
            continue;
        }
        match best {
            Some((_, current)) if current <= distance_squared => {}
            _ => best = Some((index, distance_squared)),
        }
    }
    best.map(|(index, _)| index)
}

fn push_connectors(
    world: &mut World,
    anchor: Vec2,
    discs: &[DiscScreen; 6],
    clip: Option<crate::ecs::ui::types::Rect>,
) {
    for (sorted_index, disc) in discs.iter().enumerate() {
        if !disc.is_positive {
            continue;
        }
        let mut color = disc.color;
        color.w *= CONNECTOR_DIM;
        push_segment(
            world,
            anchor,
            disc.center,
            color,
            CONNECTOR_THICKNESS_PX,
            sorted_index,
            clip,
        );
    }
}

fn push_segment(
    world: &mut World,
    from: Vec2,
    to: Vec2,
    color: Vec4,
    thickness: f32,
    sorted_index: usize,
    clip: Option<crate::ecs::ui::types::Rect>,
) {
    let delta = to - from;
    let length = (delta.x * delta.x + delta.y * delta.y).sqrt();
    if length < 0.5 {
        return;
    }
    let midpoint = (from + to) * 0.5;
    let angle = delta.y.atan2(delta.x);
    let position = midpoint - Vec2::new(length * 0.5, thickness * 0.5);
    world.resources.retained_ui.frame.rects.push(UiRect {
        position,
        size: Vec2::new(length, thickness),
        color,
        corner_radius: thickness * 0.5,
        border_width: 0.0,
        border_color: Vec4::zeros(),
        rotation: angle,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: Z_INDEX_BASE + (sorted_index as i32) * 3,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}

fn push_disc(
    world: &mut World,
    disc: &DiscScreen,
    hovered: bool,
    sorted_index: usize,
    clip: Option<crate::ecs::ui::types::Rect>,
) {
    let radius = if hovered {
        HOVER_DISC_RADIUS_PX
    } else {
        DISC_RADIUS_PX
    };
    let size = Vec2::new(radius * 2.0, radius * 2.0);
    let position = disc.center - Vec2::new(radius, radius);
    let resolved_color = if hovered { HOVER_COLOR } else { disc.color };
    let (fill_color, border_width, border_color) = if disc.is_positive || hovered {
        (resolved_color, 0.0, Vec4::zeros())
    } else {
        (NEGATIVE_FILL, NEGATIVE_BORDER_PX, resolved_color)
    };
    world.resources.retained_ui.frame.rects.push(UiRect {
        position,
        size,
        color: fill_color,
        corner_radius: radius,
        border_width,
        border_color,
        rotation: 0.0,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: Z_INDEX_BASE + (sorted_index as i32) * 3 + 1,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}

fn push_label(world: &mut World, disc: &DiscScreen, clip: Option<crate::ecs::ui::types::Rect>) {
    let dpi_scale = world.resources.window.cached_scale_factor.max(0.0001);
    let properties = TextProperties {
        font_size: LABEL_FONT_SIZE_PX * dpi_scale,
        color: LABEL_COLOR,
        alignment: TextAlignment::Center,
        vertical_alignment: VerticalAlignment::Middle,
        line_height: 1.0,
        letter_spacing: 0.0,
        outline_width: 0.0,
        outline_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
        smoothing: 0.003,
        monospace_width: None,
        anchor_character: None,
        font_kind: crate::ecs::text::resources::font_engine::FontKind::Default,
    };
    let position = Vec2::new(disc.center.x.round(), disc.center.y.round());
    world.resources.retained_ui.draw_overlay_text(
        disc.label,
        position,
        properties,
        clip,
        UiLayer::Background,
        Z_INDEX_BASE + 200,
    );
}

fn apply_orbit_drag(
    world: &mut World,
    camera_entity: Entity,
    viewport_width: f32,
    viewport_height: f32,
    mouse_delta: Vec2,
) {
    let delta_x = (mouse_delta.x / viewport_width.max(1.0)) * std::f32::consts::PI * 2.0;
    let delta_y = (mouse_delta.y / viewport_height.max(1.0)) * std::f32::consts::PI;

    if world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
        && let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity)
    {
        let signed_delta_x = if pan_orbit.is_upside_down {
            -delta_x
        } else {
            delta_x
        };
        pan_orbit.target_yaw -= signed_delta_x * pan_orbit.sensitivity.orbit;
        pan_orbit.target_pitch += delta_y * pan_orbit.sensitivity.orbit;
        if pan_orbit.limits.allow_upside_down {
            pan_orbit.target_pitch %= std::f32::consts::PI * 2.0;
        } else {
            pan_orbit.target_pitch = pan_orbit
                .target_pitch
                .clamp(pan_orbit.limits.pitch_lower, pan_orbit.limits.pitch_upper);
        }
        return;
    }

    if world
        .core
        .entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
        && let Some(third_person) = world.core.get_third_person_camera_mut(camera_entity)
    {
        third_person.target_yaw -= delta_x * third_person.orbit_sensitivity;
        third_person.target_pitch =
            (third_person.target_pitch + delta_y * third_person.orbit_sensitivity).clamp(
                third_person.pitch_lower_limit,
                third_person.pitch_upper_limit,
            );
        return;
    }

    rotate_fly_camera(world, camera_entity, delta_x, delta_y);
}

fn rotate_fly_camera(world: &mut World, camera_entity: Entity, delta_x: f32, delta_y: f32) {
    let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };
    let yaw_quat = nalgebra_glm::quat_angle_axis(-delta_x, &Vec3::y());
    local_transform.rotation = yaw_quat * local_transform.rotation;
    let forward = local_transform.forward_vector();
    let current_pitch = forward.y.asin();
    let pitch_limit = 89.0_f32.to_radians();
    let new_pitch = current_pitch + delta_y;
    if new_pitch.abs() <= pitch_limit {
        let pitch_quat = nalgebra_glm::quat_angle_axis(delta_y, &Vec3::x());
        local_transform.rotation *= pitch_quat;
    }
    mark_local_transform_dirty(world, camera_entity);
}

fn snap_camera(world: &mut World, camera_entity: Entity, axis_index: u8, is_positive: bool) {
    if try_snap_pan_orbit(world, camera_entity, axis_index, is_positive) {
        return;
    }
    if try_snap_third_person(world, camera_entity, axis_index, is_positive) {
        return;
    }
    snap_fly_camera(world, camera_entity, axis_index, is_positive);
}

fn try_snap_pan_orbit(
    world: &mut World,
    camera_entity: Entity,
    axis_index: u8,
    is_positive: bool,
) -> bool {
    if !world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        return false;
    }
    let (desired_yaw, desired_pitch) = canonical_view_angles(axis_index, is_positive);
    let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
        return false;
    };
    if let Some(yaw_value) = desired_yaw {
        pan_orbit.target_yaw = shortest_angle_target(pan_orbit.target_yaw, yaw_value);
    }
    if let Some(pitch_value) = desired_pitch {
        pan_orbit.target_pitch =
            pitch_value.clamp(pan_orbit.limits.pitch_lower, pan_orbit.limits.pitch_upper);
    }
    true
}

fn try_snap_third_person(
    world: &mut World,
    camera_entity: Entity,
    axis_index: u8,
    is_positive: bool,
) -> bool {
    if !world
        .core
        .entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
    {
        return false;
    }
    let (desired_yaw, desired_pitch) = canonical_view_angles(axis_index, is_positive);
    let Some(third_person) = world.core.get_third_person_camera_mut(camera_entity) else {
        return false;
    };
    if let Some(yaw_value) = desired_yaw {
        third_person.target_yaw = shortest_angle_target(third_person.target_yaw, yaw_value);
    }
    if let Some(pitch_value) = desired_pitch {
        third_person.target_pitch = pitch_value.clamp(
            third_person.pitch_lower_limit,
            third_person.pitch_upper_limit,
        );
    }
    true
}

fn snap_fly_camera(world: &mut World, camera_entity: Entity, axis_index: u8, is_positive: bool) {
    let snap_axis = match (axis_index, is_positive) {
        (0, true) => Vec3::new(1.0, 0.0, 0.0),
        (0, false) => Vec3::new(-1.0, 0.0, 0.0),
        (1, true) => Vec3::new(0.0, 1.0, 0.0),
        (1, false) => Vec3::new(0.0, -1.0, 0.0),
        (2, true) => Vec3::new(0.0, 0.0, 1.0),
        (2, false) => Vec3::new(0.0, 0.0, -1.0),
        _ => return,
    };
    let world_up = if snap_axis.y.abs() > 0.99 {
        Vec3::new(0.0, 0.0, 1.0)
    } else {
        Vec3::new(0.0, 1.0, 0.0)
    };

    let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };
    let current_position = local_transform.translation;
    let current_forward = local_transform.forward_vector();
    let focal = current_position + current_forward * FLY_FOCAL_DISTANCE;
    let new_position = focal + snap_axis * FLY_FOCAL_DISTANCE;
    let look_direction = nalgebra_glm::normalize(&(focal - new_position));
    let new_rotation = look_rotation(look_direction, world_up);

    local_transform.translation = new_position;
    local_transform.rotation = new_rotation;
    mark_local_transform_dirty(world, camera_entity);
}

fn look_rotation(forward: Vec3, up_hint: Vec3) -> Quat {
    let forward_normalized = nalgebra_glm::normalize(&forward);
    let right = nalgebra_glm::normalize(&nalgebra_glm::cross(&forward_normalized, &up_hint));
    let up = nalgebra_glm::cross(&right, &forward_normalized);
    let basis = Mat3::from_columns(&[right, up, -forward_normalized]);
    nalgebra_glm::mat3_to_quat(&basis)
}

fn canonical_view_angles(axis_index: u8, is_positive: bool) -> (Option<f32>, Option<f32>) {
    let pi = std::f32::consts::PI;
    let half_pi = std::f32::consts::FRAC_PI_2;
    match (axis_index, is_positive) {
        (0, true) => (Some(half_pi), Some(0.0)),
        (0, false) => (Some(-half_pi), Some(0.0)),
        (1, true) => (None, Some(half_pi)),
        (1, false) => (None, Some(-half_pi)),
        (2, true) => (Some(0.0), Some(0.0)),
        (2, false) => (Some(pi), Some(0.0)),
        _ => (None, None),
    }
}

fn shortest_angle_target(current: f32, desired: f32) -> f32 {
    let tau = std::f32::consts::TAU;
    let raw = (desired - current).rem_euclid(tau);
    let signed = if raw > std::f32::consts::PI {
        raw - tau
    } else {
        raw
    };
    current + signed
}
