//! The editor's 3D viewport: an orbit / flythrough camera and a forward
//! render of the scene.
//!
//! The engine stays UI-agnostic; this glue lives in the editor. [`Viewport`]
//! owns a [`ForwardRenderer`], a small cache of primitive [`GpuMesh`]es, two
//! camera modes ([`OrbitCamera`] for inspecting a target,
//! [`FlythroughCamera`] for free-look navigation), and draws every scene
//! entity that carries a [`MeshRenderer`](oxide_engine::render::MeshRenderer)
//! component. The mode toggle preserves pose so the camera does not snap
//! when switching.

use std::collections::HashMap;

use oxide_engine::hecs::Entity;
use oxide_engine::math::{EulerRot, Quat, Transform, Vec3};
use oxide_engine::prelude::*;
use oxide_engine::wgpu;
use oxide_engine::window::RenderCtx;

/// An orbit camera: looks at `target` from a yaw/pitch/distance offset.
pub struct OrbitCamera {
    /// The point the camera orbits and looks at.
    pub target: Vec3,
    /// Horizontal angle (radians) around `+Y`.
    pub yaw: f32,
    /// Vertical angle (radians); clamped to avoid flipping over the poles.
    pub pitch: f32,
    /// Distance from `target` to the eye.
    pub distance: f32,
}

impl Default for OrbitCamera {
    fn default() -> Self {
        Self {
            target: Vec3::new(0.0, 0.8, 0.0),
            yaw: 0.6,
            pitch: -0.45,
            distance: 12.0,
        }
    }
}

impl OrbitCamera {
    /// The camera's orientation as a quaternion.
    fn rotation(&self) -> Quat {
        Quat::from_euler(EulerRot::YXZ, self.yaw, self.pitch, 0.0)
    }

    /// The eye position in world space.
    fn eye(&self) -> Vec3 {
        self.target + self.rotation() * Vec3::new(0.0, 0.0, self.distance)
    }

    /// The camera's world transform (what the renderer takes as the view).
    pub fn view_transform(&self) -> Transform {
        Transform::looking_at(self.eye(), self.target, Vec3::Y)
    }

    /// Orbit by a pixel drag delta.
    pub fn orbit(&mut self, dx: f32, dy: f32) {
        const SENS: f32 = 0.005;
        self.yaw -= dx * SENS;
        self.pitch = (self.pitch - dy * SENS).clamp(-1.54, 1.54);
    }

    /// Pan the target in the camera's screen plane by a pixel drag delta.
    pub fn pan(&mut self, dx: f32, dy: f32) {
        let rot = self.rotation();
        let right = rot * Vec3::X;
        let up = rot * Vec3::Y;
        // Scale panning with distance so it feels consistent at any zoom.
        let speed = self.distance * 0.0015;
        self.target += (-right * dx + up * dy) * speed;
    }

    /// Zoom by a scroll delta (positive = closer).
    pub fn zoom(&mut self, amount: f32) {
        self.distance = (self.distance * (1.0 - amount * 0.1)).clamp(0.5, 500.0);
    }
}

/// A free-look "flythrough" camera: a position in world space plus a
/// yaw/pitch orientation, driven by WASD/QE translation + mouse-look in the
/// usual first-person convention.
///
/// Distinct from [`OrbitCamera`] because the two modes have fundamentally
/// different controls; switching between them preserves the camera pose via
/// [`FlythroughCamera::from_orbit`] / [`OrbitCamera::from_flythrough`] so the
/// view doesn't snap on toggle.
pub struct FlythroughCamera {
    /// Eye position in world space.
    pub position: Vec3,
    /// Horizontal angle (radians) around `+Y`, matching [`OrbitCamera::yaw`].
    pub yaw: f32,
    /// Vertical angle (radians); clamped to avoid flipping over the poles.
    pub pitch: f32,
    /// Translation speed in world units per second at the base (non-sprint)
    /// rate. Adjustable at runtime — the editor binds scroll-wheel to this.
    pub move_speed: f32,
    /// Multiplier applied while the "sprint" action is held.
    pub sprint_multiplier: f32,
}

impl Default for FlythroughCamera {
    fn default() -> Self {
        // Place the eye where the default OrbitCamera would put it, so a
        // fresh project that starts in flythrough mode (a future preference)
        // sees the same opening view.
        let orbit = OrbitCamera::default();
        Self::from_orbit(&orbit)
    }
}

impl FlythroughCamera {
    /// Position the flythrough camera to look at the same view the given
    /// orbit camera is showing. The eye lands at the orbit camera's eye
    /// position and the yaw/pitch are copied verbatim.
    pub fn from_orbit(orbit: &OrbitCamera) -> Self {
        Self {
            position: orbit.eye(),
            yaw: orbit.yaw,
            pitch: orbit.pitch,
            move_speed: 5.0,
            sprint_multiplier: 4.0,
        }
    }

    /// The camera's orientation as a quaternion (same Y-yaw-then-X-pitch
    /// convention as [`OrbitCamera::rotation`]).
    fn rotation(&self) -> Quat {
        Quat::from_euler(EulerRot::YXZ, self.yaw, self.pitch, 0.0)
    }

    /// Unit world-space forward direction (where the camera looks).
    pub fn forward(&self) -> Vec3 {
        self.rotation() * Vec3::new(0.0, 0.0, -1.0)
    }

    /// Unit world-space right direction (camera's screen-right).
    pub fn right(&self) -> Vec3 {
        self.rotation() * Vec3::X
    }

    /// Unit world-space up direction.
    pub fn up(&self) -> Vec3 {
        self.rotation() * Vec3::Y
    }

    /// The camera's world transform (what the renderer takes as the view).
    pub fn view_transform(&self) -> Transform {
        Transform::looking_at(self.position, self.position + self.forward(), Vec3::Y)
    }

    /// Mouse-look by a pixel drag delta. Same sensitivity as
    /// [`OrbitCamera::orbit`] so the gesture feels identical in both modes.
    pub fn look(&mut self, dx: f32, dy: f32) {
        const SENS: f32 = 0.005;
        self.yaw -= dx * SENS;
        self.pitch = (self.pitch - dy * SENS).clamp(-1.54, 1.54);
    }

    /// Translate by a per-frame move vector in **camera-local** axes (`+X`
    /// right, `+Y` up, `-Z` forward — the same convention game code uses for
    /// a first-person move input). Each axis is expected to be in `[-1, 1]`,
    /// the natural range of an [`AxisBinding`](oxide_engine::input::AxisBinding).
    pub fn translate_local(&mut self, local: Vec3, dt: f32, sprint: bool) {
        if local.length_squared() == 0.0 {
            return;
        }
        let speed = if sprint {
            self.move_speed * self.sprint_multiplier
        } else {
            self.move_speed
        };
        // `local` is in camera-local axes (right / up / forward). Convert to
        // world by combining with the camera basis. `-Z` is forward, so a
        // local.z of `-1.0` (from a "forward" axis) moves along +forward.
        let world = self.right() * local.x + self.up() * local.y + self.forward() * (-local.z);
        self.position += world * (speed * dt);
    }

    /// Adjust the base move speed by a scroll-wheel delta. Clamped so the
    /// camera never becomes immobile or too fast to control.
    pub fn adjust_move_speed(&mut self, scroll_lines: f32) {
        let factor = (1.0 + scroll_lines * 0.1).max(0.1);
        self.move_speed = (self.move_speed * factor).clamp(0.5, 200.0);
    }
}

impl OrbitCamera {
    /// Position an orbit camera so it shows the same view as the given
    /// flythrough camera. The target is placed [`OrbitCamera::distance`]
    /// units in front of the flythrough's eye along its forward direction.
    pub fn from_flythrough(fly: &FlythroughCamera) -> Self {
        let distance = OrbitCamera::default().distance;
        Self {
            target: fly.position + fly.forward() * distance,
            yaw: fly.yaw,
            pitch: fly.pitch,
            distance,
        }
    }
}

/// Which input scheme drives the viewport camera.
///
/// [`Orbit`](Self::Orbit) is the default editor convention — useful for
/// inspecting a single subject. [`Flythrough`](Self::Flythrough) is a
/// first-person fly: WASD/QE translate, right-drag looks around, scroll
/// adjusts move speed; better for navigating a level or open scene.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CameraMode {
    Orbit,
    Flythrough,
}

/// Owns the renderer, primitive mesh cache, camera, and lighting for the editor
/// viewport.
pub struct Viewport {
    pipeline: RenderPipeline,
    meshes: HashMap<PrimitiveShape, GpuMesh>,
    pub camera: Camera,
    pub orbit: OrbitCamera,
    pub flythrough: FlythroughCamera,
    pub mode: CameraMode,
    pub lighting: Lighting,
}

impl Viewport {
    /// Builds the viewport, uploading a GPU mesh for every primitive shape.
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let meshes = PrimitiveShape::ALL
            .iter()
            .map(|&shape| (shape, shape.mesh().upload(device, shape.label())))
            .collect();
        // The editor clears the frame before drawing the scene, so the viewport
        // pipeline is just the forward pass; post passes slot in here later.
        let mut pipeline = RenderPipeline::new();
        pipeline.add_pass("forward", ForwardPass::new(device, color_format));
        let orbit = OrbitCamera::default();
        let flythrough = FlythroughCamera::from_orbit(&orbit);
        Self {
            pipeline,
            meshes,
            camera: Camera::default(),
            orbit,
            flythrough,
            mode: CameraMode::Orbit,
            lighting: Lighting::default(),
        }
    }

    /// The view transform of the **active** camera (whichever mode is
    /// currently selected).
    pub fn view_transform(&self) -> Transform {
        match self.mode {
            CameraMode::Orbit => self.orbit.view_transform(),
            CameraMode::Flythrough => self.flythrough.view_transform(),
        }
    }

    /// Swaps between orbit and flythrough modes while preserving pose, so
    /// the visible scene does not jump when the user toggles. Returns the
    /// new mode for the caller to surface in the status bar.
    pub fn toggle_camera_mode(&mut self) -> CameraMode {
        match self.mode {
            CameraMode::Orbit => {
                self.flythrough = FlythroughCamera::from_orbit(&self.orbit);
                self.mode = CameraMode::Flythrough;
            }
            CameraMode::Flythrough => {
                self.orbit = OrbitCamera::from_flythrough(&self.flythrough);
                self.mode = CameraMode::Orbit;
            }
        }
        self.mode
    }

    /// Renders the scene's renderable entities into the frame, before the editor
    /// UI is painted on top.
    ///
    /// `viewport_rect` restricts drawing and projection to the Viewport
    /// tab's sub-rectangle of the surface (in physical pixels). `None`
    /// falls back to the full surface — handy for early frames before
    /// egui has reported a rect, and for any host that wants to render
    /// edge-to-edge.
    pub fn render(
        &mut self,
        scene: &Scene,
        ctx: &RenderCtx<'_>,
        viewport_rect: Option<oxide_engine::math::Rect>,
    ) {
        // Snapshot renderables first so the query borrow is released before we
        // resolve world transforms.
        let renderables: Vec<(Entity, MeshRenderer)> = scene
            .world()
            .query::<&MeshRenderer>()
            .iter()
            .map(|(e, mr)| (e, *mr))
            .collect();

        let view = self.view_transform();
        let mut objects = Vec::with_capacity(renderables.len());
        for (entity, mr) in &renderables {
            // Hierarchical: a disabled ancestor hides its whole subtree.
            if !scene.is_effectively_enabled(*entity).unwrap_or(true) {
                continue;
            }
            // Per-component: the MeshRenderer itself may be marked disabled
            // (e.g. by a script before a trigger fires).
            if scene.is_component_disabled(*entity, "MeshRenderer") {
                continue;
            }
            // Honor the camera's layer visibility: entities default to the
            // Default layer when they carry no explicit `Layer` component.
            let layers = scene.get::<Layer>(*entity).map(|l| *l).unwrap_or_default();
            if !self.camera.sees(layers) {
                continue;
            }
            let Some(world) = scene.world_transform(*entity) else {
                continue;
            };
            if let Some(mesh) = self.meshes.get(&mr.shape) {
                objects.push(RenderObject {
                    mesh,
                    material: mr.material,
                    transform: world,
                });
            }
        }

        self.pipeline.render(&mut FrameContext {
            device: ctx.gpu.device(),
            queue: ctx.gpu.queue(),
            color: ctx.view,
            size: ctx.size,
            viewport_rect,
            clear_color: Color::BLACK, // editor clears separately; unused here
            camera: &self.camera,
            view_transform: &view,
            lighting: &self.lighting,
            objects: &objects,
        });
    }

    /// Builds the world-space ray from `cursor` (window-physical pixels)
    /// through the viewport using the active camera's projection. The same
    /// helper feeds both entity picking and gizmo handle hit-testing — they
    /// must agree on the math or a click on a handle won't line up with
    /// what the user sees.
    pub fn ray_from_cursor(
        &self,
        cursor: (f32, f32),
        size: (u32, u32),
        viewport_rect: Option<oxide_engine::math::Rect>,
    ) -> Ray {
        let rect = viewport_rect.unwrap_or_else(|| {
            oxide_engine::math::Rect::from_min_size(
                oxide_engine::math::Vec2::ZERO,
                oxide_engine::math::Vec2::new(size.0.max(1) as f32, size.1.max(1) as f32),
            )
        });
        let (w, h) = (rect.width().max(1.0), rect.height().max(1.0));
        // Cursor is in window coords; rebase to viewport-local before NDC.
        let local_x = cursor.0 - rect.min.x;
        let local_y = cursor.1 - rect.min.y;
        // Cursor → normalized device coordinates (flip Y: screen down, NDC up).
        let ndc_x = 2.0 * local_x / w - 1.0;
        let ndc_y = 1.0 - 2.0 * local_y / h;

        let view = self.view_transform();
        let inv_vp = self.camera.view_projection(w / h, &view).inverse();
        // Unproject the near and far points of the pixel into world space.
        let near = inv_vp.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
        let far = inv_vp.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
        Ray::new(near, (far - near).normalize_or_zero())
    }

    /// The combined view-projection matrix the viewport uses for `viewport_rect`'s
    /// aspect ratio. Exposed so the gizmo overlay can project world points
    /// to screen pixels with the same math the renderer drew with.
    pub fn view_projection_for(
        &self,
        viewport_rect: Option<oxide_engine::math::Rect>,
        size: (u32, u32),
    ) -> oxide_engine::math::Mat4 {
        let rect = viewport_rect.unwrap_or_else(|| {
            oxide_engine::math::Rect::from_min_size(
                oxide_engine::math::Vec2::ZERO,
                oxide_engine::math::Vec2::new(size.0.max(1) as f32, size.1.max(1) as f32),
            )
        });
        let aspect = rect.width().max(1.0) / rect.height().max(1.0);
        let view = self.view_transform();
        self.camera.view_projection(aspect, &view)
    }

    /// Picks the nearest renderable entity under the cursor (physical pixels),
    /// by casting a ray through the viewport and testing each entity's
    /// world-space bounds. Returns `None` if the ray hits nothing.
    ///
    /// `viewport_rect` is the same sub-rectangle the render path used (the
    /// Viewport tab in the editor's case); the cursor is converted to NDC
    /// relative to it so a click at the tab's edge corresponds to the ray
    /// through that edge — not through the corresponding spot in a full-
    /// window projection. `None` falls back to the full window.
    pub fn pick(
        &self,
        scene: &Scene,
        cursor: (f32, f32),
        size: (u32, u32),
        viewport_rect: Option<oxide_engine::math::Rect>,
    ) -> Option<Entity> {
        let ray = self.ray_from_cursor(cursor, size, viewport_rect);

        let renderables: Vec<(Entity, MeshRenderer)> = scene
            .world()
            .query::<&MeshRenderer>()
            .iter()
            .map(|(e, mr)| (e, *mr))
            .collect();

        let mut best: Option<(f32, Entity)> = None;
        for (entity, mr) in renderables {
            // Don't pick what isn't visible (effectively disabled subtree, or
            // a per-component disable on the MeshRenderer).
            if scene.is_component_disabled(entity, "MeshRenderer") {
                continue;
            }
            if !scene.is_effectively_enabled(entity).unwrap_or(true) {
                continue;
            }
            let Some(world) = scene.world_transform(entity) else {
                continue;
            };
            let aabb = transform_aabb(&world, &mr.shape.local_bounds());
            if let Some(t) = aabb.ray_intersection(&ray) {
                if best.map_or(true, |(bt, _)| t < bt) {
                    best = Some((t, entity));
                }
            }
        }
        best.map(|(_, e)| e)
    }
}

/// The world-space AABB of a local AABB transformed by `t` (transform its 8
/// corners and re-fit).
fn transform_aabb(t: &Transform, local: &oxide_engine::math::Aabb) -> oxide_engine::math::Aabb {
    oxide_engine::math::Aabb::from_points(local.corners().iter().map(|&c| t.transform_point(c)))
}
