//! The concrete 3D / `f32` event-handler / physics-hooks `#[pyclass]` types and
//! the `register_events_hooks` function used by `#[pymodule]`.
//!
//! This module used to be generated by a `(Real, DIM)`-parameterized macro so
//! that several cdylibs could each expand their own concrete types. There is
//! now a single concrete target — 3D, `f32` — so the types are written out
//! directly, with no macros.
//!
//! It depends on:
//!   - The `Real`/`DIM` aliases and `Py{Vector,Point,...}` adapters.
//!   - The user-facing math types (`Vec3`, `Point3`, ...).
//!   - The geometry types — in particular `CollisionEvent`, `ContactForceEvent`,
//!     `ContactPair`, `ContactManifold`, `SolverContact`, `ColliderHandle`.
//!   - The dynamics types — `RigidBodyHandle`.
//!
//! # Threading model
//!
//! `PhysicsPipeline.step` releases the GIL via `Python::allow_threads`. The
//! rapier solver may then invoke `EventHandler` / `PhysicsHooks` trait methods
//! from a worker thread. Our adapter types (`PyEventHandler`, `PyPhysicsHooks`,
//! `ChannelEventCollector`) hold `Py<PyAny>` callables; each callback wraps the
//! call in `Python::with_gil(...)` to re-acquire the GIL before touching any
//! Python object. That is also what makes the adapters `Sync`, as the engine's
//! worker threads require: `Py<PyAny>` and `Arc<Mutex<..>>` are `Sync` on their
//! own, so no `unsafe impl` is involved.
//!
//! Exceptions raised inside Python callbacks are **deferred**: they're stashed
//! into a shared `Mutex<Option<PyErr>>` and re-raised after `step()` returns to
//! Python (the user can flip `world.event_error_policy = "strict"` to also set
//! an abort flag so the solver bails out early — but rapier doesn't currently
//! support mid-step aborts, so "strict" simply makes the error get re-raised
//! *and* future hook calls during the same step early-return without invoking
//! the user callback).

use crate::pyo3::exceptions::PyTypeError;
use crate::pyo3::pyclass::CompareOp;
use crate::*;
use rapier3d as rapier;

use std::sync::{Arc, Mutex};

// =====================================================================
// Deferred-error policy.
// =====================================================================

/// How to handle Python exceptions raised inside a callback.
///
/// - `Defer` (default): catch, stash, re-raise after `step()` returns.
/// - `Strict`: catch, stash, set an abort flag so subsequent hook calls
///   within the same `step()` short-circuit. The solver still runs to
///   completion (rapier doesn't support mid-step aborts), but the
///   error is re-raised after `step()` returns.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventErrorPolicy {
    Defer,
    Strict,
}

/// Slot shared between every adapter wrapper and the world. Stores the
/// first exception raised by a Python callback during a step, plus the
/// error policy chosen by the user.
#[derive(Default)]
pub struct DeferredError {
    pub err: Option<PyErr>,
    pub aborted: bool,
    pub policy_strict: bool,
}

// =====================================================================
// SolverFlags — bitflags wrapper.
// =====================================================================

/// Bitflags returned from ``PhysicsHooks.filter_contact_pair`` to
/// control how a contact pair is solved.
///
/// Combine flags with ``|``, intersect with ``&``, test membership with
/// ``in`` or :meth:`contains`. ``COMPUTE_IMPULSES`` is the default; an
/// empty flag set disables impulse computation for that pair (it will
/// still generate collision events).
#[pyclass(name = "SolverFlags", module = "rapier", frozen)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SolverFlags(pub rapier::geometry::SolverFlags);

#[pymethods]
impl SolverFlags {
    /// Construct a ``SolverFlags`` from a raw bit pattern.
    ///
    /// :param bits: Raw bit pattern. Bits that don't correspond to a
    ///     known flag are silently truncated.
    #[new]
    #[pyo3(signature = (bits = 0))]
    fn new(bits: u32) -> Self {
        Self(rapier::geometry::SolverFlags::from_bits_truncate(bits))
    }
    /// Return an empty ``SolverFlags`` (no contact solving).
    #[staticmethod]
    fn empty() -> Self {
        Self(rapier::geometry::SolverFlags::empty())
    }
    /// Return the default ``SolverFlags`` (``COMPUTE_IMPULSES``).
    #[staticmethod]
    fn default_() -> Self {
        Self(rapier::geometry::SolverFlags::default())
    }

    /// Compute impulses for this contact pair (i.e. solve the contact).
    #[classattr]
    const COMPUTE_IMPULSES: SolverFlags =
        SolverFlags(rapier::geometry::SolverFlags::COMPUTE_IMPULSES);
    /// Empty flag set — equivalent to :meth:`empty`.
    #[classattr]
    const EMPTY: SolverFlags = SolverFlags(rapier::geometry::SolverFlags::empty());

    /// Raw underlying bit pattern.
    #[getter]
    fn bits(&self) -> u32 {
        self.0.bits()
    }
    /// Return ``True`` if every bit set in ``other`` is also set here.
    fn contains(&self, other: &Self) -> bool {
        self.0.contains(other.0)
    }
    /// Return ``True`` if no flag is set.
    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
    /// ``other in self`` — alias for :meth:`contains`.
    fn __contains__(&self, other: &Self) -> bool {
        self.0.contains(other.0)
    }
    /// Bitwise OR — union of two flag sets.
    fn __or__(&self, other: &Self) -> Self {
        Self(self.0 | other.0)
    }
    /// Bitwise AND — intersection of two flag sets.
    fn __and__(&self, other: &Self) -> Self {
        Self(self.0 & other.0)
    }
    /// Bitwise XOR — symmetric difference.
    fn __xor__(&self, other: &Self) -> Self {
        Self(self.0 ^ other.0)
    }
    /// Bitwise NOT — flip every known flag.
    fn __invert__(&self) -> Self {
        Self(!self.0)
    }
    /// Truthy iff at least one flag is set.
    fn __bool__(&self) -> bool {
        !self.0.is_empty()
    }
    /// Equality on the raw bit pattern; other comparisons raise.
    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
        match op {
            CompareOp::Eq => Ok(self.0 == other.0),
            CompareOp::Ne => Ok(self.0 != other.0),
            _ => Err(PyTypeError::new_err("SolverFlags supports only == and !=")),
        }
    }
    /// Hash on the raw bit pattern (makes ``SolverFlags`` set-safe).
    fn __hash__(&self) -> u64 {
        self.0.bits() as u64
    }
    /// Debug repr — shows the raw bit pattern in binary.
    fn __repr__(&self) -> String {
        format!("SolverFlags(bits={:#010b})", self.0.bits())
    }
}

// =====================================================================
// PairFilterContext — transient view, only valid during the callback.
//
// We keep raw pointers to the rapier-side context fields. The
// `valid` flag is flipped to `false` immediately after the Python
// callback returns; subsequent accesses raise.
// =====================================================================

/// Context passed to ``PhysicsHooks.filter_contact_pair`` /
/// ``filter_intersection_pair``.
///
/// Exposes the two colliders and (when applicable) their parent rigid
/// bodies. The instance is a **transient view** valid only for the
/// duration of the callback that received it — keeping a reference
/// around and accessing it later raises ``RuntimeError``.
#[pyclass(name = "PairFilterContext", module = "rapier", unsendable)]
pub struct PairFilterContext {
    collider1: rapier::geometry::ColliderHandle,
    collider2: rapier::geometry::ColliderHandle,
    rigid_body1: Option<rapier::dynamics::RigidBodyHandle>,
    rigid_body2: Option<rapier::dynamics::RigidBodyHandle>,
}

#[pymethods]
impl PairFilterContext {
    /// Handle of the first collider in the pair.
    #[getter]
    fn collider1(&self) -> ColliderHandle {
        ColliderHandle(self.collider1)
    }
    /// Handle of the second collider in the pair.
    #[getter]
    fn collider2(&self) -> ColliderHandle {
        ColliderHandle(self.collider2)
    }
    /// Handle of ``collider1``'s parent rigid body, or ``None``.
    #[getter]
    fn rigid_body1(&self) -> Option<RigidBodyHandle> {
        self.rigid_body1.map(RigidBodyHandle)
    }
    /// Handle of ``collider2``'s parent rigid body, or ``None``.
    #[getter]
    fn rigid_body2(&self) -> Option<RigidBodyHandle> {
        self.rigid_body2.map(RigidBodyHandle)
    }
    /// Debug repr — shows the raw collider handles.
    fn __repr__(&self) -> String {
        format!(
            "PairFilterContext(c1={:?}, c2={:?})",
            self.collider1.into_raw_parts(),
            self.collider2.into_raw_parts(),
        )
    }
}

// =====================================================================
// ContactModificationContext — transient mutable view.
//
// The rapier callback gives us `&mut Vec<SolverContact>`, `&mut Vector`,
// `&mut u32`. We stash raw pointers and a `valid` flag. Mutations from
// Python persist back to the rapier-side buffer.
// =====================================================================

/// Context passed to ``PhysicsHooks.modify_solver_contacts``.
///
/// Lets the user inspect and mutate the solver contacts of a manifold
/// before they're handed to the constraint solver: clear them to skip
/// the contact, flip the normal, tweak per-contact user data, etc.
///
/// Like :class:`PairFilterContext`, the instance is a **transient
/// view** valid only inside the callback — accessing mutating fields
/// (``normal``, ``user_data``, ``solver_contacts``) after the callback
/// returns raises ``RuntimeError``.
#[pyclass(name = "ContactModificationContext", module = "rapier", unsendable)]
pub struct ContactModificationContext {
    collider1: rapier::geometry::ColliderHandle,
    collider2: rapier::geometry::ColliderHandle,
    rigid_body1: Option<rapier::dynamics::RigidBodyHandle>,
    rigid_body2: Option<rapier::dynamics::RigidBodyHandle>,
    // Read-only snapshot of `manifold.local_n1` (we don't expose the
    // full manifold mutably — it would entangle lifetimes too deeply).
    local_n1: rapier::math::Vector,
    local_n2: rapier::math::Vector,
    normal: *mut rapier::math::Vector,
    solver_contacts: *mut Vec<rapier::geometry::SolverContact>,
    friction: *mut Real,
    restitution: *mut Real,
    user_data: *mut u32,
    valid: bool,
}

impl ContactModificationContext {
    #[inline]
    fn check_valid(&self) -> PyResult<()> {
        if !self.valid {
            return Err(crate::pyo3::exceptions::PyRuntimeError::new_err(
                "ContactModificationContext is no longer valid (used outside of callback)",
            ));
        }
        Ok(())
    }
}

#[pymethods]
impl ContactModificationContext {
    /// Handle of the first collider in the pair.
    #[getter]
    fn collider1(&self) -> ColliderHandle {
        ColliderHandle(self.collider1)
    }
    /// Handle of the second collider in the pair.
    #[getter]
    fn collider2(&self) -> ColliderHandle {
        ColliderHandle(self.collider2)
    }
    /// Handle of ``collider1``'s parent rigid body, or ``None``.
    #[getter]
    fn rigid_body1(&self) -> Option<RigidBodyHandle> {
        self.rigid_body1.map(RigidBodyHandle)
    }
    /// Handle of ``collider2``'s parent rigid body, or ``None``.
    #[getter]
    fn rigid_body2(&self) -> Option<RigidBodyHandle> {
        self.rigid_body2.map(RigidBodyHandle)
    }
    /// Read-only snapshot of the manifold's local normal on ``collider1``.
    #[getter]
    fn local_n1(&self) -> Vec3 {
        let v: crate::na::SVector<Real, 3> = self.local_n1.into();
        Vec3(v)
    }
    /// Read-only snapshot of the manifold's local normal on ``collider2``.
    #[getter]
    fn local_n2(&self) -> Vec3 {
        let v: crate::na::SVector<Real, 3> = self.local_n2.into();
        Vec3(v)
    }

    /// Mutable world-space contact normal (read).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[getter]
    fn normal(&self) -> PyResult<Vec3> {
        self.check_valid()?;
        // SAFETY: `valid` is true exactly during the callback, where
        // the rapier-side normal is reachable through this raw pointer.
        let v: rapier::math::Vector = unsafe { *self.normal };
        let nav: crate::na::SVector<Real, 3> = v.into();
        Ok(Vec3(nav))
    }
    /// Mutable world-space contact normal (write).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[setter]
    fn set_normal(&mut self, v: PyVector) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            *self.normal = v.0.into();
        }
        Ok(())
    }

    /// Per-manifold user data, mutable from inside the callback (read).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[getter]
    fn user_data(&self) -> PyResult<u32> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        Ok(unsafe { *self.user_data })
    }
    /// Per-manifold user data (write).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[setter]
    fn set_user_data(&mut self, v: u32) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            *self.user_data = v;
        }
        Ok(())
    }

    /// Friction coefficient applied to every solver contact of this
    /// manifold (read).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[getter]
    fn friction(&self) -> PyResult<Real> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        Ok(unsafe { *self.friction })
    }
    /// Per-manifold friction coefficient (write).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[setter]
    fn set_friction(&mut self, v: Real) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            *self.friction = v;
        }
        Ok(())
    }
    /// Restitution coefficient applied to every solver contact of this
    /// manifold (read).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[getter]
    fn restitution(&self) -> PyResult<Real> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        Ok(unsafe { *self.restitution })
    }
    /// Per-manifold restitution coefficient (write).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[setter]
    fn set_restitution(&mut self, v: Real) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            *self.restitution = v;
        }
        Ok(())
    }

    /// Return the current solver contacts as a list (read-only snapshot).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    #[getter]
    fn solver_contacts(&self) -> PyResult<Vec<SolverContact>> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        let vec_ref: &Vec<rapier::geometry::SolverContact> = unsafe { &*self.solver_contacts };
        let mut out = Vec::with_capacity(vec_ref.len());
        for sc in vec_ref {
            out.push({
                // Inside the modification hook the anchors hold the fresh
                // world-space contact points; is-new lives in bit 31 of the
                // contact id.
                let p1: crate::na::Vector3<Real> = sc.anchor1.into();
                let p2: crate::na::Vector3<Real> = sc.anchor2.into();
                let id = sc.contact_id[0];
                SolverContact {
                    point: Point3(crate::na::Point3::from(p1)),
                    point2: Point3(crate::na::Point3::from(p2)),
                    dist: sc.dist,
                    contact_id: id & !rapier::geometry::NEW_CONTACT_BIT,
                    is_new: (id & rapier::geometry::NEW_CONTACT_BIT) != 0,
                }
            });
        }
        Ok(out)
    }

    /// Clear all solver contacts (the manifold won't contribute to the solver).
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    fn clear_solver_contacts(&mut self) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            (*self.solver_contacts).clear();
        }
        Ok(())
    }

    /// Number of solver contacts currently in the manifold.
    ///
    /// :raises RuntimeError: If accessed outside the callback.
    fn num_solver_contacts(&self) -> PyResult<usize> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        Ok(unsafe { (*self.solver_contacts).len() })
    }

    /// Drop the solver contact at index ``i``. Out-of-range indices are ignored.
    ///
    /// :param i: Zero-based index into the solver-contact list.
    /// :raises RuntimeError: If accessed outside the callback.
    fn remove_solver_contact(&mut self, i: usize) -> PyResult<()> {
        self.check_valid()?;
        // SAFETY: see `normal` getter.
        unsafe {
            let v = &mut *self.solver_contacts;
            if i < v.len() {
                v.remove(i);
            }
        }
        Ok(())
    }

    /// Emulate a one-way platform contact (port of rapier's helper).
    ///
    /// The "oneway" behavior only allows contacts when the local
    /// contact normal of ``collider1`` is roughly aligned with
    /// ``allowed_local_n1`` (within ``angle`` radians). Contacts
    /// pointing the wrong way are cleared so the constraint solver
    /// ignores them — letting the body pass through from the other
    /// side.
    ///
    /// :param allowed_local_n1: Local-space normal that should let the
    ///     contact through (e.g. ``Vec3(0, 1, 0)`` for a floor-only
    ///     platform).
    /// :param angle: Half-cone tolerance in radians.
    /// :raises RuntimeError: If accessed outside the callback.
    fn update_as_oneway_platform(
        &mut self,
        allowed_local_n1: PyVector,
        angle: Real,
    ) -> PyResult<()> {
        self.check_valid()?;
        // Reconstruct a `ContactModificationContext` borrow shape on
        // the fly and dispatch. We re-implement the logic inline to
        // avoid the manifold borrow.
        //
        // SAFETY: see `normal` getter.
        let allowed_n1: rapier::math::Vector = allowed_local_n1.0.into();
        let cang = Real::cos(angle);
        let contact_is_ok = self.local_n1.dot(allowed_n1) >= cang;

        const UNKNOWN: u32 = 0;
        const ALLOWED: u32 = 1;
        const FORBIDDEN: u32 = 2;

        let (sc, ud) = unsafe { (&mut *self.solver_contacts, &mut *self.user_data) };
        match *ud {
            UNKNOWN => {
                if contact_is_ok {
                    *ud = ALLOWED;
                } else {
                    sc.clear();
                    if self.local_n1.length_squared() > 0.1 {
                        *ud = FORBIDDEN;
                    }
                }
            }
            FORBIDDEN => {
                if contact_is_ok && sc.iter().all(|c| c.dist > 0.0) {
                    *ud = ALLOWED;
                } else {
                    sc.clear();
                }
            }
            ALLOWED if sc.is_empty() => {
                *ud = UNKNOWN;
            }
            _ => {}
        }
        Ok(())
    }

    /// Debug repr — shows the raw collider handles.
    fn __repr__(&self) -> String {
        format!(
            "ContactModificationContext(c1={:?}, c2={:?})",
            self.collider1.into_raw_parts(),
            self.collider2.into_raw_parts(),
        )
    }
}

// =====================================================================
// PyEventHandler — adapts an arbitrary Python object implementing the
// `EventHandler` protocol to rapier's `EventHandler` trait.
// =====================================================================

/// Adapter wrapping an arbitrary Python `EventHandler`-protocol object.
pub struct PyEventHandler {
    obj: Py<PyAny>,
    err_slot: Arc<Mutex<DeferredError>>,
}

impl PyEventHandler {
    pub fn new(obj: Py<PyAny>, err_slot: Arc<Mutex<DeferredError>>) -> Self {
        Self { obj, err_slot }
    }
}

impl rapier::pipeline::EventHandler for PyEventHandler {
    fn handle_collision_event(
        &self,
        _bodies: &rapier::dynamics::RigidBodySet,
        _colliders: &rapier::geometry::ColliderSet,
        event: rapier::geometry::CollisionEvent,
        contact_pair: Option<&rapier::geometry::ContactPair>,
    ) {
        {
            let slot = self.err_slot.lock().unwrap();
            if slot.aborted {
                return;
            }
        }
        Python::with_gil(|py| {
            let py_event = CollisionEvent::from_rapier(event);
            let py_pair: PyObject = match contact_pair {
                Some(p) => Py::new(py, ContactPair(p.clone()))
                    .map(|h| h.into_any())
                    .unwrap_or_else(|_| py.None()),
                None => py.None(),
            };
            let res = self.obj.bind(py).call_method1(
                "handle_collision_event",
                (py.None(), py.None(), py_event, py_pair),
            );
            if let Err(e) = res {
                let mut s = self.err_slot.lock().unwrap();
                if s.err.is_none() {
                    s.err = Some(e);
                }
                if s.policy_strict {
                    s.aborted = true;
                }
            }
        });
    }

    fn handle_contact_force_event(
        &self,
        dt: Real,
        _bodies: &rapier::dynamics::RigidBodySet,
        _colliders: &rapier::geometry::ColliderSet,
        contact_pair: &rapier::geometry::ContactPair,
        total_force_magnitude: Real,
    ) {
        {
            let slot = self.err_slot.lock().unwrap();
            if slot.aborted {
                return;
            }
        }
        Python::with_gil(|py| {
            let py_pair = match Py::new(py, ContactPair(contact_pair.clone())) {
                Ok(p) => p,
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    return;
                }
            };
            let res = self.obj.bind(py).call_method1(
                "handle_contact_force_event",
                (dt, py.None(), py.None(), py_pair, total_force_magnitude),
            );
            if let Err(e) = res {
                let mut s = self.err_slot.lock().unwrap();
                if s.err.is_none() {
                    s.err = Some(e);
                }
                if s.policy_strict {
                    s.aborted = true;
                }
            }
        });
    }
}

// =====================================================================
// ChannelEventCollector — built-in EventHandler that funnels events
// into internal buffers, drained on demand from Python.
// =====================================================================

/// Recommended built-in event handler.
///
/// A :class:`ChannelEventCollector` accumulates collision and
/// contact-force events into lock-free internal buffers. Attach it to
/// a world via ``world.event_handler``, then call
/// :meth:`drain_collision_events` / :meth:`drain_contact_force_events`
/// after each step to consume them::
///
///     c = ChannelEventCollector()
///     world.event_handler = c
///     world.step()
///     for event in c.drain_collision_events():
///         ...
///
/// Because the buffers are owned by Rust and the pipeline detects the
/// concrete type, this avoids the per-event Python callback overhead
/// of a generic ``EventHandler`` protocol object.
#[pyclass(name = "ChannelEventCollector", module = "rapier")]
pub struct ChannelEventCollector {
    collisions: Arc<Mutex<Vec<CollisionEvent>>>,
    forces: Arc<Mutex<Vec<ContactForceEvent>>>,
}

#[pymethods]
impl ChannelEventCollector {
    /// Construct an empty collector with no events queued.
    #[new]
    fn new() -> Self {
        Self {
            collisions: Arc::new(Mutex::new(Vec::new())),
            forces: Arc::new(Mutex::new(Vec::new())),
        }
    }

    /// Consume and return every queued :class:`CollisionEvent`.
    ///
    /// After this call the internal collision buffer is empty.
    ///
    /// :returns: A list of collision events captured since the last
    ///     drain.
    fn drain_collision_events(&self) -> Vec<CollisionEvent> {
        std::mem::take(&mut *self.collisions.lock().unwrap())
    }

    /// Consume and return every queued :class:`ContactForceEvent`.
    ///
    /// After this call the internal contact-force buffer is empty.
    ///
    /// :returns: A list of contact-force events captured since the
    ///     last drain.
    fn drain_contact_force_events(&self) -> Vec<ContactForceEvent> {
        std::mem::take(&mut *self.forces.lock().unwrap())
    }

    /// Drop every queued collision and contact-force event.
    fn clear(&self) {
        self.collisions.lock().unwrap().clear();
        self.forces.lock().unwrap().clear();
    }

    /// Total queued events (collisions + contact-force).
    fn __len__(&self) -> usize {
        self.collisions.lock().unwrap().len() + self.forces.lock().unwrap().len()
    }

    /// Debug repr — shows the per-buffer queue lengths.
    fn __repr__(&self) -> String {
        let ce = self.collisions.lock().unwrap().len();
        let fe = self.forces.lock().unwrap().len();
        format!("ChannelEventCollector(collisions={ce}, forces={fe})")
    }
}

impl ChannelEventCollector {
    /// Build an `EventHandler`-implementing adapter that pushes into
    /// this collector's buffers. The adapter is `Sync`, as the engine's worker
    /// threads require.
    pub fn as_event_handler(&self) -> ChannelEventCollectorAdapter {
        ChannelEventCollectorAdapter {
            collisions: Arc::clone(&self.collisions),
            forces: Arc::clone(&self.forces),
        }
    }
}

/// `EventHandler`-implementing adapter built on the fly by
/// `ChannelEventCollector::as_event_handler`. Holds `Arc<Mutex<...>>`
/// clones of the collector's buffers, so it can outlive a single call.
pub struct ChannelEventCollectorAdapter {
    collisions: Arc<Mutex<Vec<CollisionEvent>>>,
    forces: Arc<Mutex<Vec<ContactForceEvent>>>,
}

impl rapier::pipeline::EventHandler for ChannelEventCollectorAdapter {
    fn handle_collision_event(
        &self,
        _bodies: &rapier::dynamics::RigidBodySet,
        _colliders: &rapier::geometry::ColliderSet,
        event: rapier::geometry::CollisionEvent,
        _contact_pair: Option<&rapier::geometry::ContactPair>,
    ) {
        self.collisions
            .lock()
            .unwrap()
            .push(CollisionEvent::from_rapier(event));
    }

    fn handle_contact_force_event(
        &self,
        dt: Real,
        _bodies: &rapier::dynamics::RigidBodySet,
        _colliders: &rapier::geometry::ColliderSet,
        contact_pair: &rapier::geometry::ContactPair,
        total_force_magnitude: Real,
    ) {
        let r = rapier::geometry::ContactForceEvent::from_contact_pair(
            dt,
            contact_pair,
            total_force_magnitude,
        );
        let v: crate::na::SVector<Real, 3> = r.total_force.into();
        let d: crate::na::SVector<Real, 3> = r.max_force_direction.into();
        self.forces.lock().unwrap().push(ContactForceEvent {
            collider1: ColliderHandle(r.collider1),
            collider2: ColliderHandle(r.collider2),
            total_force: Vec3(v),
            total_force_magnitude: r.total_force_magnitude,
            max_force_direction: Vec3(d),
            max_force_magnitude: r.max_force_magnitude,
        });
    }
}

// =====================================================================
// PyPhysicsHooks — adapter for the `PhysicsHooks` Python protocol.
// =====================================================================

/// Adapter wrapping an arbitrary Python `PhysicsHooks`-protocol object.
pub struct PyPhysicsHooks {
    obj: Py<PyAny>,
    err_slot: Arc<Mutex<DeferredError>>,
}

impl PyPhysicsHooks {
    pub fn new(obj: Py<PyAny>, err_slot: Arc<Mutex<DeferredError>>) -> Self {
        Self { obj, err_slot }
    }
}

impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks {
    fn filter_contact_pair(
        &self,
        context: &rapier::pipeline::PairFilterContext,
    ) -> Option<rapier::geometry::SolverFlags> {
        {
            let slot = self.err_slot.lock().unwrap();
            if slot.aborted {
                return Some(rapier::geometry::SolverFlags::default());
            }
        }
        Python::with_gil(|py| {
            let ctx = match Py::new(
                py,
                PairFilterContext {
                    collider1: context.collider1,
                    collider2: context.collider2,
                    rigid_body1: context.rigid_body1,
                    rigid_body2: context.rigid_body2,
                },
            ) {
                Ok(c) => c,
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    return Some(rapier::geometry::SolverFlags::default());
                }
            };
            let res = self
                .obj
                .bind(py)
                .call_method1("filter_contact_pair", (ctx,));
            match res {
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    Some(rapier::geometry::SolverFlags::default())
                }
                Ok(v) => {
                    if v.is_none() {
                        None
                    } else if let Ok(f) = v.extract::<SolverFlags>() {
                        Some(f.0)
                    } else if let Ok(bits) = v.extract::<u32>() {
                        Some(rapier::geometry::SolverFlags::from_bits_truncate(bits))
                    } else {
                        // Any other truthy return is treated as "default".
                        Some(rapier::geometry::SolverFlags::default())
                    }
                }
            }
        })
    }

    fn filter_intersection_pair(&self, context: &rapier::pipeline::PairFilterContext) -> bool {
        {
            let slot = self.err_slot.lock().unwrap();
            if slot.aborted {
                return true;
            }
        }
        Python::with_gil(|py| {
            let ctx = match Py::new(
                py,
                PairFilterContext {
                    collider1: context.collider1,
                    collider2: context.collider2,
                    rigid_body1: context.rigid_body1,
                    rigid_body2: context.rigid_body2,
                },
            ) {
                Ok(c) => c,
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    return true;
                }
            };
            match self
                .obj
                .bind(py)
                .call_method1("filter_intersection_pair", (ctx,))
            {
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    true
                }
                Ok(v) => v.extract::<bool>().unwrap_or(true),
            }
        })
    }

    fn modify_solver_contacts(&self, context: &mut rapier::pipeline::ContactModificationContext) {
        {
            let slot = self.err_slot.lock().unwrap();
            if slot.aborted {
                return;
            }
        }
        Python::with_gil(|py| {
            let manifold_local_n1 = context.manifold.local_n1;
            let manifold_local_n2 = context.manifold.local_n2;
            let normal_ptr: *mut rapier::math::Vector = context.normal;
            let sc_ptr: *mut Vec<rapier::geometry::SolverContact> = context.solver_contacts;
            let friction_ptr: *mut Real = context.friction;
            let restitution_ptr: *mut Real = context.restitution;
            let ud_ptr: *mut u32 = context.user_data;
            let ctx_py = match Py::new(
                py,
                ContactModificationContext {
                    collider1: context.collider1,
                    collider2: context.collider2,
                    rigid_body1: context.rigid_body1,
                    rigid_body2: context.rigid_body2,
                    local_n1: manifold_local_n1,
                    local_n2: manifold_local_n2,
                    normal: normal_ptr,
                    solver_contacts: sc_ptr,
                    friction: friction_ptr,
                    restitution: restitution_ptr,
                    user_data: ud_ptr,
                    valid: true,
                },
            ) {
                Ok(c) => c,
                Err(e) => {
                    let mut s = self.err_slot.lock().unwrap();
                    if s.err.is_none() {
                        s.err = Some(e);
                    }
                    if s.policy_strict {
                        s.aborted = true;
                    }
                    return;
                }
            };
            let res = self
                .obj
                .bind(py)
                .call_method1("modify_solver_contacts", (ctx_py.clone_ref(py),));
            // Invalidate the transient view before letting Python see
            // its `None` return.
            {
                let mut borrowed = ctx_py.borrow_mut(py);
                borrowed.valid = false;
            }
            if let Err(e) = res {
                let mut s = self.err_slot.lock().unwrap();
                if s.err.is_none() {
                    s.err = Some(e);
                }
                if s.policy_strict {
                    s.aborted = true;
                }
            }
        });
    }
}

// =====================================================================
// Public helpers used by `pipeline.rs` to construct the adapters.
// =====================================================================

/// Wrap an arbitrary Python object as a (boxed) rapier `EventHandler`,
/// or detect a `ChannelEventCollector` and short-circuit to its
/// lock-free buffer adapter.
///
/// Returns `None` if `obj` is `Py::None` / Python `None`.
pub fn build_event_handler(
    py: Python<'_>,
    obj: Option<&Py<PyAny>>,
    err_slot: Arc<Mutex<DeferredError>>,
) -> Option<Box<dyn rapier::pipeline::EventHandler>> {
    let obj = obj?;
    let bound = obj.bind(py);
    if bound.is_none() {
        return None;
    }
    // ChannelEventCollector: optimize away the Python callback entirely.
    if let Ok(c) = bound.extract::<PyRef<'_, ChannelEventCollector>>() {
        return Some(Box::new(c.as_event_handler()));
    }
    Some(Box::new(PyEventHandler::new(obj.clone_ref(py), err_slot)))
}

/// Wrap an arbitrary Python object as a (boxed) rapier `PhysicsHooks`.
pub fn build_physics_hooks(
    py: Python<'_>,
    obj: Option<&Py<PyAny>>,
    err_slot: Arc<Mutex<DeferredError>>,
) -> Option<Box<dyn rapier::pipeline::PhysicsHooks>> {
    let obj = obj?;
    let bound = obj.bind(py);
    if bound.is_none() {
        return None;
    }
    Some(Box::new(PyPhysicsHooks::new(obj.clone_ref(py), err_slot)))
}

pub fn register_events_hooks(
    _py: crate::pyo3::Python<'_>,
    m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>,
) -> crate::pyo3::PyResult<()> {
    use crate::pyo3::prelude::*;
    m.add_class::<SolverFlags>()?;
    m.add_class::<PairFilterContext>()?;
    m.add_class::<ContactModificationContext>()?;
    m.add_class::<ChannelEventCollector>()?;
    Ok(())
}
