//! Orbit camera for inspecting 3D assets.
//!
//! Controls:
//!   - Left-click + drag → orbit
//!   - Right-click + drag → pan
//!   - Scroll wheel       → zoom
//!   - R                  → reset to default view

use bevy::prelude::*;
use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::render::render_asset::RenderAssetUsages;
use bevy::render::render_resource::{
    Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension,
};

pub struct OrbitCameraPlugin;

impl Plugin for OrbitCameraPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, spawn_camera)
           .add_systems(Update, orbit_camera);
    }
}

/// State for the orbit camera controller.
#[derive(Component)]
pub struct OrbitCamera {
    /// Distance from the focus point
    pub radius: f32,
    /// Rotation around the vertical axis (azimuth) in radians
    pub yaw: f32,
    /// Rotation around the horizontal axis (elevation) in radians
    pub pitch: f32,
    /// The point the camera orbits around
    pub focus: Vec3,
    pub orbit_sensitivity: f32,
    pub zoom_sensitivity: f32,
    pub pan_sensitivity: f32,
    /// Zoom limits — set when framing a model so large scenes can be pulled back
    /// far enough (the old fixed 0.5..50 cap trapped the camera inside big stages).
    pub min_radius: f32,
    pub max_radius: f32,
}

impl Default for OrbitCamera {
    fn default() -> Self {
        Self {
            radius: 5.0,
            yaw: std::f32::consts::FRAC_PI_4,
            pitch: std::f32::consts::FRAC_PI_6,
            focus: Vec3::ZERO,
            orbit_sensitivity: 0.005,
            zoom_sensitivity: 0.3,
            pan_sensitivity: 0.003,
            min_radius: 0.05,
            max_radius: 500.0,
        }
    }
}

fn spawn_camera(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
    let orbit = OrbitCamera::default();
    let transform = orbit_transform(&orbit);

    // The game's ship shader reflects a shared HDR environment cubemap off the
    // hull (three cube samples — see the shader RE). Reproduce that "look" with a
    // procedural sky cube feeding Bevy's image-based lighting, so metallic
    // surfaces reflect an environment instead of reading flat. Procedural (not a
    // bundled KTX2) so it also works on WASM with no extra assets.
    let env = make_env_cubemap(&mut images);

    commands.spawn((
        Camera3d::default(),
        // Wide near/far so both tiny weapons and multi-thousand-unit stages fit;
        // the planes are re-scaled to the zoom distance each frame (below).
        Projection::Perspective(PerspectiveProjection {
            near: 0.05,
            far: 100_000.0,
            ..default()
        }),
        transform,
        orbit,
        EnvironmentMapLight {
            diffuse_map: env.clone(),
            specular_map: env,
            intensity: 900.0,
            ..default()
        },
    ));
}

/// Build a small procedural sky cubemap (6 faces) for image-based lighting.
///
/// A vertical gradient (bright zenith → warm horizon → dark ground) plus a warm
/// "sun" highlight roughly where the game's key light sits — enough for a
/// metallic hull to read as reflective rather than flat. Stored as an sRGB cube
/// (filterable, no half-float encoding needed); a single mip means reflections
/// stay sharp (fine for a shiny ship).
fn make_env_cubemap(images: &mut Assets<Image>) -> Handle<Image> {
    let size: i32 = 64;
    // Approx key-light direction from the RE (PS c32 ≈ (0,-0.76,-0.65), i.e. light
    // arriving from top-front); place the bright spot there.
    let sun = Vec3::new(0.0, 0.85, 0.5).normalize();
    let zenith = Vec3::new(0.70, 0.80, 0.95);
    let horizon = Vec3::new(0.42, 0.45, 0.50);
    let ground = Vec3::new(0.09, 0.09, 0.11);
    let srgb = |c: f32| (c.clamp(0.0, 1.0).powf(1.0 / 2.2) * 255.0).round() as u8;

    let mut data: Vec<u8> = Vec::with_capacity((size * size * 6 * 4) as usize);
    for face in 0..6 {
        for y in 0..size {
            for x in 0..size {
                let u = (x as f32 + 0.5) / size as f32 * 2.0 - 1.0;
                let v = (y as f32 + 0.5) / size as f32 * 2.0 - 1.0;
                // Standard cube-face → direction mapping.
                let d = match face {
                    0 => Vec3::new(1.0, -v, -u),
                    1 => Vec3::new(-1.0, -v, u),
                    2 => Vec3::new(u, 1.0, v),
                    3 => Vec3::new(u, -1.0, -v),
                    4 => Vec3::new(u, -v, 1.0),
                    _ => Vec3::new(-u, -v, -1.0),
                }
                .normalize();
                let mut col = if d.y >= 0.0 {
                    horizon.lerp(zenith, (d.y).powf(0.6))
                } else {
                    horizon.lerp(ground, (-d.y).powf(0.5))
                };
                let s = d.dot(sun).max(0.0).powf(60.0);
                col += Vec3::new(1.0, 0.85, 0.6) * s * 1.5;
                data.extend_from_slice(&[srgb(col.x), srgb(col.y), srgb(col.z), 255]);
            }
        }
    }

    let mut image = Image::new(
        Extent3d {
            width: size as u32,
            height: size as u32,
            depth_or_array_layers: 6,
        },
        TextureDimension::D2,
        data,
        TextureFormat::Rgba8UnormSrgb,
        RenderAssetUsages::RENDER_WORLD,
    );
    image.texture_view_descriptor = Some(TextureViewDescriptor {
        dimension: Some(TextureViewDimension::Cube),
        ..default()
    });
    images.add(image)
}

fn orbit_camera(
    mut query: Query<(&mut OrbitCamera, &mut Transform, &mut Projection)>,
    mouse_buttons: Res<ButtonInput<MouseButton>>,
    keys: Res<ButtonInput<KeyCode>>,
    mut mouse_motion: EventReader<MouseMotion>,
    mut scroll: EventReader<MouseWheel>,
) {
    let Ok((mut cam, mut transform, mut projection)) = query.get_single_mut() else { return };

    let mut delta_motion = Vec2::ZERO;
    for ev in mouse_motion.read() {
        delta_motion += ev.delta;
    }

    let mut scroll_delta = 0.0f32;
    for ev in scroll.read() {
        scroll_delta += ev.y;
    }

    // Orbit (left mouse drag)
    if mouse_buttons.pressed(MouseButton::Left) {
        cam.yaw   -= delta_motion.x * cam.orbit_sensitivity;
        cam.pitch -= delta_motion.y * cam.orbit_sensitivity;
        // Clamp pitch to avoid gimbal lock
        cam.pitch = cam.pitch.clamp(-1.5, 1.5);
    }

    // Pan (right mouse drag)
    if mouse_buttons.pressed(MouseButton::Right) {
        let right = transform.rotation * Vec3::X;
        let up    = transform.rotation * Vec3::Y;
        // Copy fields before mutably borrowing `cam.focus`
        let pan_sens = cam.pan_sensitivity;
        let radius   = cam.radius;
        cam.focus -= right * delta_motion.x * pan_sens * radius;
        cam.focus += up    * delta_motion.y * pan_sens * radius;
    }

    // Zoom (scroll)
    cam.radius -= scroll_delta * cam.zoom_sensitivity * cam.radius;
    cam.radius = cam.radius.clamp(cam.min_radius, cam.max_radius);

    // Reset (R key)
    if keys.just_pressed(KeyCode::KeyR) {
        *cam = OrbitCamera::default();
    }

    // Keep the clip planes proportional to the zoom distance so depth precision
    // stays usable across scales (tiny prop → whole stage grid).
    if let Projection::Perspective(p) = projection.as_mut() {
        p.near = (cam.radius * 0.02).clamp(0.02, 50.0);
        p.far = (cam.radius * 50.0).max(2000.0);
    }

    *transform = orbit_transform(&cam);
}

fn orbit_transform(cam: &OrbitCamera) -> Transform {
    let rotation = Quat::from_euler(EulerRot::YXZ, cam.yaw, cam.pitch, 0.0);
    let offset = rotation * Vec3::new(0.0, 0.0, cam.radius);
    Transform::from_translation(cam.focus + offset)
        .looking_at(cam.focus, Vec3::Y)
}
