// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;

impl Scene {
    // ── MSPACE helpers ───────────────────────────────────────────────────

    /// Convert a **paper-space** world coordinate to **model-space** using the
    /// geometry of the currently active viewport.  Returns the input unchanged
    /// when there is no active viewport.
    /// Convert a paper-space point to model space (precise at UTM scale).
    pub fn paper_to_model(&self, paper_pt: glam::DVec3) -> glam::DVec3 {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return paper_pt,
        };
        let vp = match self.document.get_entity(vp_handle) {
            Some(acadrust::EntityType::Viewport(vp)) => vp,
            _ => return paper_pt,
        };
        // Uses the viewport's own `view_target` — kept valid by
        // `normalize_active_viewport_view` on entry, which folds a stale UTM
        // saved view onto the auto-fit centre so the display, pan/zoom and this
        // inverse all agree. Cheap (no per-call camera rebuild).
        let scale = vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
        if scale.abs() < 1e-9 {
            return paper_pt;
        }
        let tx = vp.view_target.x;
        let ty = vp.view_target.y;
        let pcx = vp.center.x;
        let pcy = vp.center.y;
        glam::DVec3::new(
            (paper_pt.x - pcx) / scale + tx,
            (paper_pt.y - pcy) / scale + ty,
            paper_pt.z,
        )
    }

    /// Inverse of [`paper_to_model`]: map a model-space point to the paper
    /// sheet through the active viewport. Returns the input unchanged when
    /// there is no active viewport. Kept as the inverse companion to
    /// `paper_to_model`; in-viewport overlays now project via the viewport
    /// camera ([`viewport_edit_frame`]) rather than mapping onto the sheet.
    #[allow(dead_code)]
    pub fn model_to_paper(&self, model_pt: glam::DVec3) -> glam::DVec3 {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return model_pt,
        };
        let vp = match self.document.get_entity(vp_handle) {
            Some(acadrust::EntityType::Viewport(vp)) => vp,
            _ => return model_pt,
        };
        let scale = vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
        glam::DVec3::new(
            (model_pt.x - vp.view_target.x) * scale + vp.center.x,
            (model_pt.y - vp.view_target.y) * scale + vp.center.y,
            model_pt.z,
        )
    }

    /// In-viewport (MSPACE) editing frame: the active floating viewport's own
    /// camera — *exactly* the one the GPU renders its content with
    /// ([`camera_for_viewport`]) — together with the viewport's full screen
    /// rectangle in canvas pixels ([`viewport_screen_rect`]).
    ///
    /// This is the unified editing adapter (the "süzgeç"). Inside a viewport,
    /// editing IS model-space: treat the returned camera as *the* camera, the
    /// returned rect as *the* pane, and the cursor relative to that rect — then
    /// the existing model-space snap / hit-test / grip / preview / plane-pick
    /// code runs unchanged and lands on the same pixels the GPU draws. Results
    /// come back as model coordinates directly (no paper round-trip).
    ///
    /// Because the camera is the real GPU camera, this tracks the viewport's
    /// pan / zoom / twist / oblique view correctly — unlike a linear
    /// paper-projection, whose auto-fit / saved-view / crop divergence left the
    /// snap stale after pan/zoom. Returns `None` when not editing inside a
    /// floating viewport, or the camera / rect cannot be derived.
    pub fn viewport_edit_frame(
        &self,
        canvas_px: (f32, f32),
    ) -> Option<(view::camera::Camera, iced::Rectangle)> {
        let vp_handle = self.active_viewport?;
        let cam = self.camera_for_viewport(vp_handle)?;
        let full = self.viewport_screen_rect(vp_handle, canvas_px)?;
        Some((cam, full))
    }

    /// Fold the active viewport's saved view onto the effective camera (the
    /// auto-fit centre for stale UTM views) and persist it into `view_target` /
    /// `view_height`. Called on entering MSPACE so pan/zoom, paper↔model and the
    /// rendered content all share one valid view — otherwise a stale `(0,0,0)`
    /// target left the camera auto-fitting to the model centre while the cursor
    /// math used the origin, and pan toggled the two (jitter).
    pub fn normalize_active_viewport_view(&mut self) {
        let Some(vp_handle) = self.active_viewport else {
            return;
        };
        let Some(cam) = self.camera_for_viewport(vp_handle) else {
            return;
        };
        let eff_h = cam.ortho_size() as f64 * 2.0;
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            vp.view_target.x = cam.target.x;
            vp.view_target.y = cam.target.y;
            vp.view_center.x = 0.0;
            vp.view_center.y = 0.0;
            if eff_h > 1e-9 {
                vp.view_height = eff_h;
            }
        }
    }

    /// Pan the active viewport's model-space view by `(screen_dx, screen_dy)` pixels.
    /// The delta is converted to model-space units using the camera and viewport scale.
    /// No-op when there is no active viewport.
    pub fn pan_active_viewport(&mut self, screen_dx: f32, screen_dy: f32, bounds: iced::Rectangle) {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return,
        };
        // Use the viewport's own camera for the pan axes (matches 3-D view orientation).
        let vp_cam = match self.camera_for_viewport(vp_handle) {
            Some(c) => c,
            None => return,
        };

        // Read viewport dims (immutable borrow ends here).
        let (view_height, vp_height, locked) = match self.document.get_entity(vp_handle) {
            Some(acadrust::EntityType::Viewport(vp)) => {
                (vp.view_height as f32, vp.height as f32, vp.status.locked)
            }
            _ => return,
        };
        if locked {
            return;
        }

        // Correct pan speed: how many model units correspond to one screen pixel.
        //
        // The paper camera's ortho_size() gives the visible paper-space half-height
        // (in paper mm). One screen pixel = 2*half_h / canvas_height paper mm.
        // Inside the viewport, one paper mm = view_height / vp_height model units.
        // Together: model_per_pixel = (2*half_h / canvas_height) * (view_height / vp_height)
        let paper_half_h = self.camera.borrow().ortho_size();
        let speed = if bounds.height > 0.0 && paper_half_h > 1e-6 && vp_height > 1e-6 {
            (2.0 * paper_half_h / bounds.height) * (view_height / vp_height)
        } else {
            vp_cam.distance * 0.001
        };

        let cam_right = vp_cam.rotation * glam::Vec3::X;
        let cam_up = vp_cam.rotation * glam::Vec3::Y;
        let model_delta = -(cam_right * screen_dx * speed) + (cam_up * screen_dy * speed);

        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            vp.view_target.x += model_delta.x as f64;
            vp.view_target.y += model_delta.y as f64;
            vp.view_target.z += model_delta.z as f64;
        }
    }

    /// Zoom the active viewport's model-space view by `steps` notches.
    /// Positive = zoom in (increase detail), negative = zoom out.
    /// `cursor_paper`: optional paper-space XY of the cursor; when supplied the
    /// model point under the cursor is kept stationary (AutoCAD-style zoom).
    /// No-op when there is no active viewport.
    pub fn zoom_active_viewport(&mut self, steps: f32, cursor_paper: Option<glam::Vec2>) {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return,
        };
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            if vp.status.locked {
                return;
            }
            // Zoom in = shrink view_height → higher scale → objects appear larger.
            let factor = (1.0_f64 - 0.15 * steps as f64).clamp(0.1, 10.0);

            if let Some(cp) = cursor_paper {
                // Compute the model-space point under the cursor before zoom.
                let scale_before =
                    vp_effective_scale(vp.custom_scale, vp.view_height, vp.height) as f32;
                let cx = vp.center.x as f32;
                let cy = vp.center.y as f32;
                let tx = vp.view_target.x as f32;
                let ty = vp.view_target.y as f32;
                let mx = (cp.x - cx) / scale_before + tx;
                let my = (cp.y - cy) / scale_before + ty;

                // Apply zoom.
                vp.view_height = (vp.view_height * factor).max(1e-6);
                if vp.view_height.abs() > 1e-9 {
                    vp.custom_scale = vp.height / vp.view_height;
                }
                let scale_after = vp.custom_scale as f32;

                // Adjust view_target so the model point under cursor stays there.
                let mx_after = (cp.x - cx) / scale_after + vp.view_target.x as f32;
                let my_after = (cp.y - cy) / scale_after + vp.view_target.y as f32;
                vp.view_target.x += (mx - mx_after) as f64;
                vp.view_target.y += (my - my_after) as f64;
            } else {
                vp.view_height = (vp.view_height * factor).max(1e-6);
                if vp.view_height.abs() > 1e-9 {
                    vp.custom_scale = vp.height / vp.view_height;
                }
            }
        }
    }

    /// Orbit the active viewport's view direction by the given screen-pixel delta.
    /// No-op when there is no active viewport or it is locked.
    pub fn orbit_active_viewport(&mut self, delta_x: f32, delta_y: f32) {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return,
        };
        let mut cam = match self.camera_for_viewport(vp_handle) {
            Some(c) => c,
            None => return,
        };
        // Floating viewport orbits about its own target (no selection pivot).
        cam.orbit(delta_x, delta_y, None);
        // yaw_pitch_to_quat(y,p)*Z = (cos(p)*sin(y), -cos(p)*cos(y), sin(p))
        // `camera_for_viewport` reconstructs the rotation so that
        // `rotation * Z == view_direction` exactly (its `yaw = atan2(x, -y)`
        // cancels the sign). Store `eye` directly so the orbit round-trips —
        // negating Y here made each drag step read back a Y-mirrored camera,
        // flipping the model between a rotation and its opposite every frame.
        let eye = cam.rotation * glam::Vec3::Z;
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            if vp.status.locked {
                return;
            }
            vp.view_direction.x = eye.x as f64;
            vp.view_direction.y = eye.y as f64;
            vp.view_direction.z = eye.z as f64;
        }
    }

    /// Snap the active viewport's view direction to `eye_dir` (unit
    /// vector from target toward camera). Twist angle is left at its
    /// current value so the up-sense is preserved across successive
    /// snaps. No-op when there is no active viewport or it is locked.
    pub fn snap_active_viewport_to_direction(&mut self, eye_dir: glam::Vec3, ucs: glam::Mat4) {
        let vp_handle = match self.active_viewport {
            Some(h) => h,
            None => return,
        };
        // Build the full UCS-aligned orientation exactly as the model snap does
        // (snap_to_direction picks the in-plane roll from the UCS axes), seeded
        // from the viewport's current camera so the "best up" stays stable, then
        // decode it back to the stored (view_direction, twist_angle). Writing
        // only view_direction loses the roll and the rebuilt camera snaps to
        // WCS-up instead of the UCS the clicked cube was drawn in.
        let mut tmp = self.camera_for_viewport(vp_handle).unwrap_or_default();
        tmp.snap_to_direction(eye_dir, ucs);
        let dir = (tmp.rotation * glam::Vec3::Z).normalize_or(glam::Vec3::Z);
        let desired_up = (tmp.rotation * glam::Vec3::Y).normalize_or(glam::Vec3::Y);

        // camera_from_view rebuilds the rotation with its OWN yaw convention
        // (atan2(x, -y)) and applies roll = -twist, which is *not* the camera's
        // internal yaw/roll convention — so `-tmp.roll()` does not round-trip.
        // Instead reproduce the decoder's zero-twist basis here, then measure
        // the signed roll about the view axis that carries its up onto the
        // desired UCS up. Store twist = -roll (the decoder negates it back).
        let pitch = dir.z.clamp(-1.0, 1.0).asin();
        let yaw = if dir.x.abs() < 1e-6 && dir.y.abs() < 1e-6 {
            0.0
        } else {
            dir.x.atan2(-dir.y)
        };
        let up0 = (view::camera::yaw_pitch_to_quat(yaw, pitch, 0.0) * glam::Vec3::Y)
            .normalize_or(glam::Vec3::Y);
        let roll = up0.cross(desired_up).dot(dir).atan2(up0.dot(desired_up));
        let twist = -roll as f64;
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            if vp.status.locked {
                return;
            }
            vp.view_direction.x = dir.x as f64;
            vp.view_direction.y = dir.y as f64;
            vp.view_direction.z = dir.z as f64;
            vp.twist_angle = twist;
        }
    }

    /// Mutate the active viewport's camera through a closure, then re-encode the
    /// result to the stored `(view_direction, twist_angle)` — the same decode
    /// the ViewCube snap uses. Lets the home / roll / nudge controls drive a
    /// floating viewport just like the model camera. Returns `false` if there is
    /// no active (unlocked) viewport.
    pub fn mutate_active_viewport_camera(
        &mut self,
        f: impl FnOnce(&mut view::camera::Camera),
    ) -> bool {
        let Some(vp_handle) = self.active_viewport else {
            return false;
        };
        let mut tmp = self.camera_for_viewport(vp_handle).unwrap_or_default();
        f(&mut tmp);
        let dir = (tmp.rotation * glam::Vec3::Z).normalize_or(glam::Vec3::Z);
        let desired_up = (tmp.rotation * glam::Vec3::Y).normalize_or(glam::Vec3::Y);
        let pitch = dir.z.clamp(-1.0, 1.0).asin();
        let yaw = if dir.x.abs() < 1e-6 && dir.y.abs() < 1e-6 {
            0.0
        } else {
            dir.x.atan2(-dir.y)
        };
        let up0 = (view::camera::yaw_pitch_to_quat(yaw, pitch, 0.0) * glam::Vec3::Y)
            .normalize_or(glam::Vec3::Y);
        let roll = up0.cross(desired_up).dot(dir).atan2(up0.dot(desired_up));
        let twist = -roll as f64;
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
            if vp.status.locked {
                return false;
            }
            vp.view_direction.x = dir.x as f64;
            vp.view_direction.y = dir.y as f64;
            vp.view_direction.z = dir.z as f64;
            vp.twist_angle = twist;
            return true;
        }
        false
    }

    /// Render mode of the active paper-space viewport, or `None` when no
    /// viewport is active (PSPACE / model layout).
    pub fn active_viewport_render_mode(
        &self,
    ) -> Option<acadrust::entities::ViewportRenderMode> {
        let h = self.active_viewport?;
        match self.document.get_entity(h) {
            Some(acadrust::EntityType::Viewport(vp)) => Some(vp.render_mode),
            _ => None,
        }
    }

    /// Set the active paper-space viewport's render mode. Returns `true`
    /// when a viewport was active and updated; `false` (no-op) otherwise,
    /// so the caller can fall back to the model-layout render mode.
    pub fn set_active_viewport_render_mode(
        &mut self,
        mode: acadrust::entities::ViewportRenderMode,
    ) -> bool {
        let Some(h) = self.active_viewport else {
            return false;
        };
        if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(h) {
            vp.render_mode = mode;
            true
        } else {
            false
        }
    }

    /// Visual style of the active Model tile (for the render-mode picker).
    pub fn active_model_tile_render_mode(
        &self,
    ) -> acadrust::entities::ViewportRenderMode {
        let tiles = self.model_tiles.borrow();
        let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
        tiles
            .get(active)
            .map(|t| t.render_mode)
            .unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D)
    }

    /// Set only the active Model tile's render mode. Other tiles keep theirs.
    pub fn set_active_model_tile_render_mode(
        &self,
        mode: acadrust::entities::ViewportRenderMode,
    ) {
        let mut tiles = self.model_tiles.borrow_mut();
        let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
        if let Some(t) = tiles.get_mut(active) {
            t.render_mode = mode;
        }
    }

    /// Current gaze direction (canonical +Z eye dir, world space) of whichever
    /// camera owns the ViewCube — the active floating viewport in MSPACE, else
    /// the main camera. Used by the ViewCube "already there → flip to opposite"
    /// check, which must test the camera the cube actually reflects (the paper
    /// camera always looks straight down, so testing it flipped every snap).
    pub fn active_gaze_dir(&self) -> glam::Vec3 {
        if let Some(h) = self.active_viewport {
            if let Some(cam) = self.camera_for_viewport(h) {
                return cam.rotation * glam::Vec3::Z;
            }
        }
        self.camera.borrow().rotation * glam::Vec3::Z
    }

    /// View-rotation matrix for the active viewport (MSPACE), or the
    /// paper-space camera's matrix when not in MSPACE.
    /// Used by ViewCube hit-testing so clicks map to the correct camera.
    pub fn active_view_rotation_mat(&self) -> glam::Mat4 {
        // Must match exactly what the drawn cube uses (see ViewportData's
        // `cam_rotation`): the active context's camera composed with the
        // ViewCube UCS. Inside a floating viewport that's the viewport's own
        // camera; the UCS factor applies in both cases.
        if let Some(h) = self.active_viewport {
            if let Some(cam) = self.camera_for_viewport(h) {
                return cam.view_rotation_mat() * self.viewcube_ucs_mat();
            }
        }
        self.camera.borrow().view_rotation_mat() * self.viewcube_ucs_mat()
    }

    /// The UCS→world rotation the ViewCube should compose with the camera —
    /// the active UCS in model space, identity everywhere else. Render,
    /// hit-test, and click-snap all go through this so they stay in lock-step.
    pub fn viewcube_ucs_mat(&self) -> glam::Mat4 {
        // UCS applies in model space and inside a floating viewport (MSPACE);
        // plain paper space stays WCS.
        if self.current_layout == "Model" || self.active_viewport.is_some() {
            self.viewcube_ucs
        } else {
            glam::Mat4::IDENTITY
        }
    }

    /// Return the handle of the user viewport whose *visible* on-screen
    /// rectangle (clamped to the canvas) contains the given screen-pixel point.
    /// Viewport activation goes through this so a click only enters a viewport
    /// when it lands on the part the user can actually see — clicking the empty
    /// area beside a viewport that runs off-screen no longer matches its full
    /// (partly off-canvas) paper rect and switches to it by mistake.
    pub fn viewport_at_screen_point(
        &self,
        px: f32,
        py: f32,
        canvas: (f32, f32),
    ) -> Option<Handle> {
        let (_, _, handles) = self.paper_viewport_handles();
        handles
            .iter()
            .filter_map(|handle| {
                let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
                    return None;
                };
                if !vp.status.is_on {
                    return None;
                }
                let rect = self.viewport_screen_rect(vp.common.handle, canvas)?;
                let x0 = rect.x.max(0.0);
                let y0 = rect.y.max(0.0);
                let x1 = (rect.x + rect.width).min(canvas.0);
                let y1 = (rect.y + rect.height).min(canvas.1);
                if x1 <= x0 || y1 <= y0 {
                    return None; // fully off-canvas → nothing to click
                }
                if px >= x0 && px <= x1 && py >= y0 && py <= y1 {
                    Some((vp.common.handle, (x1 - x0) * (y1 - y0)))
                } else {
                    None
                }
            })
            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
            .map(|(h, _)| h)
    }

    /// Return the handle of the first active user viewport in the current layout,
    /// or `None` if there are none.  Used by the MS command.
    pub fn first_user_viewport(&self) -> Option<Handle> {
        let (_, _, handles) = self.paper_viewport_handles();
        handles.iter().find_map(|handle| {
            let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
                return None;
            };
            if vp.status.is_on {
                Some(vp.common.handle)
            } else {
                None
            }
        })
    }
}
