// Arcball orbit camera — quaternion-based rotation, no gimbal lock.
//
// The camera orbits around a `target` point using a unit quaternion (`rotation`)
// that maps the canonical "camera looks down -Z" pose to the current view.
//
// Pan:       translates `target` in the view-plane (no rotation change).
// Orbit:     updates `rotation` via arcball delta — converts screen drag delta
//            to a rotation axis/angle, then pre-multiplies the current quaternion.
// Zoom:      adjusts `distance` (exponential feel).
// Snap:      directly assigns yaw+pitch encoded as a quaternion (for ViewCube).
//
// Coordinate convention: Z-up world space (same as the rest of OpenCADStudio).

use glam::{vec3, Mat4, Quat, Vec3};
use iced::{Point, Rectangle};

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Projection {
    Orthographic,
    Perspective,
}

#[derive(Clone)]
pub struct Camera {
    /// World-space pivot point the camera orbits around.
    pub target: Vec3,
    /// Arcball rotation: maps canonical pose to current orientation.
    pub rotation: Quat,
    /// Distance from eye to target.
    pub distance: f32,
    /// Vertical field of view in radians (perspective only).
    pub fov_y: f32,
    pub projection: Projection,

    // --- Legacy yaw/pitch exposed only for ViewCube hit-test compatibility ---
    // Kept in sync with `rotation` whenever orbit() or snap_angles() is called.
    pub yaw: f32,
    pub pitch: f32,
}

impl Default for Camera {
    fn default() -> Self {
        // Default: look straight down at the XY drawing plane (top view, Z-up).
        // yaw = 0, pitch = PI/2  →  eye is directly above target.
        let yaw = 0.0_f32;
        let pitch = std::f32::consts::FRAC_PI_2;
        Self {
            target: Vec3::ZERO,
            rotation: yaw_pitch_to_quat(yaw, pitch, 0.0),
            distance: 60.36,
            fov_y: 45.0_f32.to_radians(),
            projection: Projection::Orthographic,
            yaw,
            pitch,
        }
    }
}

pub const OPENGL_TO_WGPU: Mat4 = glam::mat4(
    glam::vec4(1.0, 0.0, 0.0, 0.0),
    glam::vec4(0.0, 1.0, 0.0, 0.0),
    glam::vec4(0.0, 0.0, 0.5, 0.0),
    glam::vec4(0.0, 0.0, 0.5, 1.0),
);

impl Camera {
    // ── Eye position ───────────────────────────────────────────────────────

    pub fn eye(&self) -> Vec3 {
        // The canonical eye direction is +Z (looking at origin from above).
        // The rotation maps that canonical pose to the current orientation.
        let eye_dir = self.rotation * Vec3::Z;
        self.target + eye_dir * self.distance
    }

    /// Half-height of the orthographic frustum in world units.
    pub fn ortho_size(&self) -> f32 {
        self.distance * (self.fov_y * 0.5).tan()
    }

    // ── Projection matrices ────────────────────────────────────────────────

    pub fn view_proj(&self, bounds: Rectangle) -> Mat4 {
        let near = self.distance * 0.001;
        let far = self.distance * 1000.0;
        let aspect = bounds.width / bounds.height;

        // Up vector: use the rotation to find which world direction is "up"
        // in the current camera frame.
        let up_dir = self.rotation * Vec3::Y;

        let view = Mat4::look_at_rh(self.eye(), self.target, up_dir);
        let proj = match self.projection {
            Projection::Perspective => Mat4::perspective_rh(self.fov_y, aspect, near, far),
            Projection::Orthographic => {
                let h = self.ortho_size();
                let w = h * aspect;
                Mat4::orthographic_rh(-w, w, -h, h, near, far)
            }
        };
        OPENGL_TO_WGPU * proj * view
    }

    /// Project a screen point onto an arbitrary world-space plane.
    ///
    /// The plane is defined by `plane_normal` (unit vector) and a `plane_point`
    /// that lies on it.  Returns the intersection of the view ray with the plane;
    /// falls back to `plane_point` when the ray is nearly parallel to the plane.
    pub fn pick_on_plane(
        &self,
        screen: Point,
        bounds: Rectangle,
        plane_normal: Vec3,
        plane_point: Vec3,
    ) -> Vec3 {
        let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
        let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
        let inv = self.view_proj(bounds).inverse();

        let (ray_origin, ray_dir) = match self.projection {
            Projection::Perspective => {
                let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
                let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
                let dir = (far_pt - near_pt).normalize();
                (near_pt, dir)
            }
            Projection::Orthographic => {
                let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
                let forward = (self.target - self.eye()).normalize();
                (origin, forward)
            }
        };

        let denom = ray_dir.dot(plane_normal);
        if denom.abs() < 1e-6 {
            return plane_point;
        }
        let t = (plane_point - ray_origin).dot(plane_normal) / denom;
        if t < 0.0 {
            return plane_point;
        }
        ray_origin + ray_dir * t
    }

    pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
        let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
        let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
        let inv = self.view_proj(bounds).inverse();

        match self.projection {
            Projection::Perspective => {
                let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
                let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
                let dir = (far_pt - near_pt).normalize();
                let forward = (self.target - self.eye()).normalize();
                let denom = dir.dot(forward);
                if denom.abs() < 1e-6 {
                    return self.target;
                }
                let t = (self.target - near_pt).dot(forward) / denom;
                if t < 0.0 {
                    return self.target;
                }
                near_pt + dir * t
            }
            Projection::Orthographic => {
                let ray_origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
                let forward = (self.target - self.eye()).normalize();
                let t = (self.target - ray_origin).dot(forward) / forward.dot(forward);
                ray_origin + forward * t
            }
        }
    }

    pub fn position_vec4(&self) -> glam::Vec4 {
        glam::Vec4::from((self.eye(), 0.0))
    }

    // ── ViewCube rotation matrix ───────────────────────────────────────────

    /// Returns the rotation matrix for the ViewCube.
    ///
    /// The camera quaternion maps canonical pose (+Z eye) → current view.
    /// The ViewCube needs the inverse so the cube stays world-aligned.
    /// Inverse of a unit quaternion = its conjugate.
    pub fn view_rotation_mat(&self) -> Mat4 {
        Mat4::from_quat(self.rotation.conjugate())
    }

    // ── Navigation ────────────────────────────────────────────────────────

    /// Arcball orbit: drag delta (dx, dy) in screen pixels.
    pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
        if delta_x.abs() < 1e-6 && delta_y.abs() < 1e-6 {
            return;
        }

        let speed = 0.005_f32;
        let angle = (delta_x * delta_x + delta_y * delta_y).sqrt() * speed;

        // Screen drag → rotation axis: right drag = rotate around cam_up (Y),
        // down drag = rotate around cam_right (X). Negate so drag direction
        // matches intuitive "grab and spin" arcball behaviour.
        let screen_axis = vec3(-delta_y, -delta_x, 0.0).normalize_or_zero();

        let cam_right = self.rotation * Vec3::X;
        let cam_up = self.rotation * Vec3::Y;
        let world_axis = (cam_right * screen_axis.x + cam_up * screen_axis.y).normalize_or_zero();

        if world_axis.length_squared() < 1e-12 {
            return;
        }

        let delta_rot = Quat::from_axis_angle(world_axis, angle);
        self.rotation = (delta_rot * self.rotation).normalize();

        // Sync legacy yaw/pitch for hit-test functions.
        self.sync_yaw_pitch();
    }

    pub fn zoom(&mut self, delta: f32) {
        self.distance = (self.distance * (1.0 - delta * 0.1)).max(0.001);
    }

    pub fn zoom_about_point(&mut self, screen: Point, bounds: Rectangle, delta: f32) {
        if bounds.width <= 0.0 || bounds.height <= 0.0 {
            self.zoom(delta);
            return;
        }

        let before = self.pick_on_target_plane(screen, bounds);
        self.zoom(delta);
        let after = self.pick_on_target_plane(screen, bounds);
        self.target += before - after;
    }

    /// Pan so the world point under the cursor tracks it: screen pixels are
    /// converted to world units via the ortho world-per-pixel scale of a
    /// viewport `viewport_height` pixels tall. Used by tiled panes where the
    /// pane height differs from the full canvas.
    pub fn pan_screen(&mut self, delta_x: f32, delta_y: f32, viewport_height: f32) {
        let wpp = if viewport_height > 0.0 {
            (2.0 * self.ortho_size()) / viewport_height
        } else {
            0.0
        };
        let cam_right = self.rotation * Vec3::X;
        let cam_up = self.rotation * Vec3::Y;
        self.target -= cam_right * delta_x * wpp;
        self.target += cam_up * delta_y * wpp;
    }

    pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3) {
        self.target = (min + max) * 0.5;
        let size = (max - min).length();
        self.distance = size * 1.5;
    }

    // ── ViewCube snap ─────────────────────────────────────────────────────

    /// Snap to a canonical view direction (called by ViewCubeSnap).
    /// `eye_dir` is the unit vector from the target toward the camera.
    ///
    /// Up vector resolution:
    ///  1. Take the current up.
    ///  2. Pick the world axis (±X, ±Y, ±Z) whose dot product with the
    ///     current up is highest — skipping any axis (anti-)parallel to
    ///     the new gaze direction.
    ///  3. Project that axis onto the plane ⊥ `new_eye` and use that as
    ///     the new up.
    ///
    /// Result: small tilts collapse onto the nearest world axis (so the
    /// view always lands cleanly aligned), while genuine flips of the
    /// up-sense (e.g. orbited upside-down) are preserved.
    pub fn snap_to_direction(&mut self, eye_dir: Vec3) {
        let new_eye = eye_dir.normalize_or(Vec3::Z);
        let cur_up = self.rotation * Vec3::Y;
        let cardinals = [
            Vec3::X,
            Vec3::NEG_X,
            Vec3::Y,
            Vec3::NEG_Y,
            Vec3::Z,
            Vec3::NEG_Z,
        ];
        let mut best_score = f32::NEG_INFINITY;
        let mut best_up = Vec3::Z;
        for axis in cardinals {
            // Skip axes (nearly) collinear with the new gaze — they can't
            // serve as up because the projection onto the plane would
            // vanish.
            if axis.dot(new_eye).abs() > 0.999 {
                continue;
            }
            let score = axis.dot(cur_up);
            if score > best_score {
                best_score = score;
                best_up = axis;
            }
        }
        // Project the chosen axis onto the plane ⊥ new_eye and normalize.
        let projected = best_up - new_eye * best_up.dot(new_eye);
        let new_up = projected.normalize_or(if new_eye.z.abs() < 0.99 {
            (Vec3::Z - new_eye * Vec3::Z.dot(new_eye)).normalize()
        } else {
            (Vec3::Y - new_eye * Vec3::Y.dot(new_eye)).normalize()
        });
        let new_right = new_up.cross(new_eye).normalize();
        // Camera rotation columns: [cam_x | cam_y | cam_z] where
        // cam_z = eye_dir (canonical "+Z is toward eye"), cam_y = up.
        let mat = glam::Mat3::from_cols(new_right, new_up, new_eye);
        self.rotation = Quat::from_mat3(&mat).normalize();
        self.sync_yaw_pitch();
    }

    // ── Internal helpers ───────────────────────────────────────────────────

    /// Derive yaw and pitch from the current quaternion for the ViewCube
    /// hit-test functions (hit_test / hover_id). These use yaw/pitch to
    /// compute the same rotation matrix as the shader, so they must match.
    fn sync_yaw_pitch(&mut self) {
        // Eye direction in world space (canonical eye dir is +Z).
        let eye_dir = self.rotation * Vec3::Z;
        // pitch: angle above/below the XY plane.
        self.pitch = eye_dir.z.clamp(-0.999, 0.999).asin();
        // yaw: atan2(x, y) matches from_rotation_z(yaw) used in view_rotation_mat.
        self.yaw = eye_dir.x.atan2(eye_dir.y);
    }
}

// ── Free helpers ───────────────────────────────────────────────────────────

/// Build a rotation quaternion from yaw (rotation around Z) and pitch
/// (tilt toward Z). Matches the coordinate convention of the ViewCube
/// so snap angles continue to work unchanged.
///
/// Convention (Z-up, Y-forward):
///   yaw   = 0          → camera looks along +Y axis (front view)
///   pitch = PI/2       → camera looks down -Z (top view)
///   pitch = 0          → camera in the XY plane
/// Build a rotation quaternion from yaw, pitch and roll.
/// Positive yaw rotates the view direction clockwise when seen from above (Z-up).
/// Roll rotates the camera around its own view axis (post-multiplied so it
/// composes after the yaw/pitch gaze direction is set).
pub fn yaw_pitch_to_quat(yaw: f32, pitch: f32, roll: f32) -> Quat {
    // +yaw so ViewCube faces match camera direction (FRONT at yaw=0 = +Y world axis).
    let q_yaw = Quat::from_rotation_z(yaw);
    let q_pitch = Quat::from_rotation_x(std::f32::consts::FRAC_PI_2 - pitch);
    let q_roll = Quat::from_rotation_z(roll);
    (q_yaw * q_pitch * q_roll).normalize()
}
