use nalgebra::{DMatrix, DVector, DVectorView};
use std::{
    cell::Cell,
    collections::{BTreeMap, BTreeSet},
    cmp::Ordering,
    fmt,
    fmt::{Debug, Formatter},
    hash::{Hash, Hasher},
    rc::Rc,
    sync::{atomic, atomic::AtomicU64}
};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */

use crate::{
    display::DisplayItem,
    engine::{
        Q,
        change_half_curvature,
        local_unif_to_std,
        point,
        project_point_to_normalized,
        project_sphere_to_normalized,
        realize_gram,
        sphere,
        ConfigSubspace,
        ConstraintProblem
    },
    outline::OutlineItem,
    specified::SpecifiedValue
};

pub type ElementColor = [f32; 3];

/* KLUDGE */
// we should reconsider this design when we build a system for switching between
// assemblies. at that point, we might want to switch to hierarchical keys,
// where each each item has a key that identifies it within its assembly and
// each assembly has a key that identifies it within the sesssion
static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);

pub trait Serial {
    // a serial number that uniquely identifies this element
    fn serial(&self) -> u64;
    
    // take the next serial number, panicking if that was the last one left
    fn next_serial() -> u64 where Self: Sized {
        // the technique we use to panic on overflow is taken from _Rust Atomics
        // and Locks_, by Mara Bos
        //
        //   https://marabos.nl/atomics/atomics.html#example-handle-overflow
        //
        NEXT_SERIAL.fetch_update(
            atomic::Ordering::SeqCst, atomic::Ordering::SeqCst,
            |serial| serial.checked_add(1)
        ).expect("Out of serial numbers for elements")
    }
}

impl Hash for dyn Serial {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.serial().hash(state)
    }
}

impl PartialEq for dyn Serial {
    fn eq(&self, other: &Self) -> bool {
        self.serial() == other.serial()
    }
}

impl Eq for dyn Serial {}

impl PartialOrd for dyn Serial {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for dyn Serial {
    fn cmp(&self, other: &Self) -> Ordering {
        self.serial().cmp(&other.serial())
    }
}

pub trait ProblemPoser {
    fn pose(&self, problem: &mut ConstraintProblem);
}

pub trait Element: Serial + ProblemPoser + DisplayItem {
    // the default identifier for an element of this type
    fn default_id() -> String where Self: Sized;
    
    // the default example of an element of this type
    fn default(id: String, id_num: u64) -> Self where Self: Sized;
    
    // the default regulators that come with this element
    fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
        Vec::new()
    }
    
    fn id(&self) -> &String;
    fn label(&self) -> &String;
    fn representation(&self) -> Signal<DVector<f64>>;
    fn ghost(&self) -> Signal<bool>;
    
    // the regulators the element is subject to. the assembly that owns the
    // element is responsible for keeping this set up to date
    fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
    
    // project a representation vector for this kind of element onto its
    // normalization variety
    fn project_to_normalized(&self, rep: &mut DVector<f64>);
    
    // the configuration matrix column index that was assigned to the element
    // last time the assembly was realized, or `None` if the element has never
    // been through a realization
    fn column_index(&self) -> Option<usize>;
    
    // assign the element a configuration matrix column index. this method must
    // be used carefully to preserve invariant (1), described in the comment on
    // the `tangent` field of the `Assembly` structure
    fn set_column_index(&self, index: usize);
}

impl Debug for dyn Element {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        self.id().fmt(f)
    }
}

impl Hash for dyn Element {
    fn hash<H: Hasher>(&self, state: &mut H) {
        <dyn Serial>::hash(self, state)
    }
}

impl PartialEq for dyn Element {
    fn eq(&self, other: &Self) -> bool {
        <dyn Serial>::eq(self, other)
    }
}

impl Eq for dyn Element {}

impl PartialOrd for dyn Element {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        <dyn Serial>::partial_cmp(self, other)
    }
}

impl Ord for dyn Element {
    fn cmp(&self, other: &Self) -> Ordering {
        <dyn Serial>::cmp(self, other)
    }
}

pub struct Sphere {
    pub id: String,
    pub label: String,
    pub color: ElementColor,
    pub representation: Signal<DVector<f64>>,
    pub ghost: Signal<bool>,
    pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
    serial: u64,
    column_index: Cell<Option<usize>>
}

impl Sphere {
    const CURVATURE_COMPONENT: usize = 3;
    
    pub fn new(
        id: String,
        label: String,
        color: ElementColor,
        representation: DVector<f64>
    ) -> Sphere {
        Sphere {
            id: id,
            label: label,
            color: color,
            representation: create_signal(representation),
            ghost: create_signal(false),
            regulators: create_signal(BTreeSet::new()),
            serial: Self::next_serial(),
            column_index: None.into()
        }
    }
}

impl Element for Sphere {
    fn default_id() -> String {
        "sphere".to_string()
    }
    
    fn default(id: String, id_num: u64) -> Sphere {
        Sphere::new(
            id,
            format!("Sphere {id_num}"),
            [0.75_f32, 0.75_f32, 0.75_f32],
            sphere(0.0, 0.0, 0.0, 1.0)
        )
    }
    
    fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
        vec![Rc::new(HalfCurvatureRegulator::new(self))]
    }
    
    fn id(&self) -> &String {
        &self.id
    }
    
    fn label(&self) -> &String {
        &self.label
    }
    
    fn representation(&self) -> Signal<DVector<f64>> {
        self.representation
    }
    
    fn ghost(&self) -> Signal<bool> {
        self.ghost
    }
    
    fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
        self.regulators
    }
    
    fn project_to_normalized(&self, rep: &mut DVector<f64>) {
        project_sphere_to_normalized(rep);
    }
    
    fn column_index(&self) -> Option<usize> {
        self.column_index.get()
    }
    
    fn set_column_index(&self, index: usize) {
        self.column_index.set(Some(index));
    }
}

impl Serial for Sphere {
    fn serial(&self) -> u64 {
        self.serial
    }
}

impl ProblemPoser for Sphere {
    fn pose(&self, problem: &mut ConstraintProblem) {
        let index = self.column_index().expect(
            format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
        );
        problem.gram.push_sym(index, index, 1.0);
        problem.guess.set_column(index, &self.representation.get_clone_untracked());
    }
}

pub struct Point {
    pub id: String,
    pub label: String,
    pub color: ElementColor,
    pub representation: Signal<DVector<f64>>,
    pub ghost: Signal<bool>,
    pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
    serial: u64,
    column_index: Cell<Option<usize>>
}

impl Point {
    const WEIGHT_COMPONENT: usize = 3;
    
    pub fn new(
        id: String,
        label: String,
        color: ElementColor,
        representation: DVector<f64>
    ) -> Point {
        Point {
            id,
            label,
            color,
            representation: create_signal(representation),
            ghost: create_signal(false),
            regulators: create_signal(BTreeSet::new()),
            serial: Self::next_serial(),
            column_index: None.into()
        }
    }
}

impl Element for Point {
    fn default_id() -> String {
        "point".to_string()
    }
    
    fn default(id: String, id_num: u64) -> Point {
        Point::new(
            id,
            format!("Point {id_num}"),
            [0.75_f32, 0.75_f32, 0.75_f32],
            point(0.0, 0.0, 0.0)
        )
    }
    
    fn id(&self) -> &String {
        &self.id
    }
    
    fn label(&self) -> &String {
        &self.label
    }
    
    fn representation(&self) -> Signal<DVector<f64>> {
        self.representation
    }
    
    fn ghost(&self) -> Signal<bool> {
        self.ghost
    }
    
    fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
        self.regulators
    }
    
    fn project_to_normalized(&self, rep: &mut DVector<f64>) {
        project_point_to_normalized(rep);
    }
    
    fn column_index(&self) -> Option<usize> {
        self.column_index.get()
    }
    
    fn set_column_index(&self, index: usize) {
        self.column_index.set(Some(index));
    }
}

impl Serial for Point {
    fn serial(&self) -> u64 {
        self.serial
    }
}

impl ProblemPoser for Point {
    fn pose(&self, problem: &mut ConstraintProblem) {
        let index = self.column_index().expect(
            format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
        );
        problem.gram.push_sym(index, index, 0.0);
        problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5);
        problem.guess.set_column(index, &self.representation.get_clone_untracked());
    }
}

pub trait Regulator: Serial + ProblemPoser + OutlineItem {
    fn subjects(&self) -> Vec<Rc<dyn Element>>;
    fn measurement(&self) -> ReadSignal<f64>;
    fn set_point(&self) -> Signal<SpecifiedValue>;
    
    // this method is used to responsively precondition the assembly for
    // realization when the regulator becomes a constraint, or is edited while
    // acting as a constraint. it should track the set point, do any desired
    // preconditioning when the set point is present, and use its return value
    // to report whether the set is present. the default implementation does no
    // preconditioning
    fn try_activate(&self) -> bool {
        self.set_point().with(|set_pt| set_pt.is_present())
    }
}

impl Hash for dyn Regulator {
    fn hash<H: Hasher>(&self, state: &mut H) {
        <dyn Serial>::hash(self, state)
    }
}

impl PartialEq for dyn Regulator {
    fn eq(&self, other: &Self) -> bool {
        <dyn Serial>::eq(self, other)
    }
}

impl Eq for dyn Regulator {}

impl PartialOrd for dyn Regulator {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        <dyn Serial>::partial_cmp(self, other)
    }
}

impl Ord for dyn Regulator {
    fn cmp(&self, other: &Self) -> Ordering {
        <dyn Serial>::cmp(self, other)
    }
}

pub struct InversiveDistanceRegulator {
    pub subjects: [Rc<dyn Element>; 2],
    pub measurement: ReadSignal<f64>,
    pub set_point: Signal<SpecifiedValue>,
    serial: u64
}

impl InversiveDistanceRegulator {
    pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator {
        let representations = subjects.each_ref().map(|subj| subj.representation());
        let measurement = create_memo(move || {
            representations[0].with(|rep_0|
                representations[1].with(|rep_1|
                    rep_0.dot(&(&*Q * rep_1))
                )
            )
        });
        
        let set_point = create_signal(SpecifiedValue::from_empty_spec());
        let serial = Self::next_serial();
        
        InversiveDistanceRegulator { subjects, measurement, set_point, serial }
    }
}

impl Regulator for InversiveDistanceRegulator {
    fn subjects(&self) -> Vec<Rc<dyn Element>> {
        self.subjects.clone().into()
    }
    
    fn measurement(&self) -> ReadSignal<f64> {
        self.measurement
    }
    
    fn set_point(&self) -> Signal<SpecifiedValue> {
        self.set_point
    }
}

impl Serial for InversiveDistanceRegulator {
    fn serial(&self) -> u64 {
        self.serial
    }
}

impl ProblemPoser for InversiveDistanceRegulator {
    fn pose(&self, problem: &mut ConstraintProblem) {
        self.set_point.with_untracked(|set_pt| {
            if let Some(val) = set_pt.value {
                let [row, col] = self.subjects.each_ref().map(
                    |subj| subj.column_index().expect(
                        "Subjects should be indexed before inversive distance regulator writes problem data"
                    )
                );
                problem.gram.push_sym(row, col, val);
            }
        });
    }
}

pub struct HalfCurvatureRegulator {
    pub subject: Rc<dyn Element>,
    pub measurement: ReadSignal<f64>,
    pub set_point: Signal<SpecifiedValue>,
    serial: u64
}

impl HalfCurvatureRegulator {
    pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
        let measurement = subject.representation().map(
            |rep| rep[Sphere::CURVATURE_COMPONENT]
        );
        
        let set_point = create_signal(SpecifiedValue::from_empty_spec());
        let serial = Self::next_serial();
        
        HalfCurvatureRegulator { subject, measurement, set_point, serial }
    }
}

impl Regulator for HalfCurvatureRegulator {
    fn subjects(&self) -> Vec<Rc<dyn Element>> {
        vec![self.subject.clone()]
    }
    
    fn measurement(&self) -> ReadSignal<f64> {
        self.measurement
    }
    
    fn set_point(&self) -> Signal<SpecifiedValue> {
        self.set_point
    }
    
    fn try_activate(&self) -> bool {
        match self.set_point.with(|set_pt| set_pt.value) {
            Some(half_curv) => {
                self.subject.representation().update(
                    |rep| change_half_curvature(rep, half_curv)
                );
                true
            }
            None => false
        }
    }
}

impl Serial for HalfCurvatureRegulator {
    fn serial(&self) -> u64 {
        self.serial
    }
}

impl ProblemPoser for HalfCurvatureRegulator {
    fn pose(&self, problem: &mut ConstraintProblem) {
        self.set_point.with_untracked(|set_pt| {
            if let Some(val) = set_pt.value {
                let col = self.subject.column_index().expect(
                    "Subject should be indexed before half-curvature regulator writes problem data"
                );
                problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
            }
        });
    }
}

// the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> {
    pub element: Rc<dyn Element>,
    pub velocity: DVectorView<'a, f64>
}

type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;

// a complete, view-independent description of an assembly
#[derive(Clone)]
pub struct Assembly {
    // elements and regulators
    pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
    pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
    
    // a flag that tells us whether the assembly is realized
    pub realized: Signal<bool>,
    
    // solution variety tangent space. the basis vectors are stored in
    // configuration matrix format, ordered according to the elements' column
    // indices. when you realize the assembly, every element that's present
    // during realization gets a column index and is reflected in the tangent
    // space. since the methods in this module never assign column indices
    // without later realizing the assembly, we get the following invariant:
    //
    //  (1) if an element has a column index, its tangent motions can be found
    //      in that column of the tangent space basis matrices
    //
    pub tangent: Signal<ConfigSubspace>,
    
    // indexing
    pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>
}

impl Assembly {
    pub fn new() -> Assembly {
        // create an assembly
        let assembly = Assembly {
            elements: create_signal(BTreeSet::new()),
            regulators: create_signal(BTreeSet::new()),
            realized: create_signal(true),
            tangent: create_signal(ConfigSubspace::zero(0)),
            elements_by_id: create_signal(BTreeMap::default())
        };
        
        // realize the assembly whenever the `realized` flag is set to `false`
        let assembly_for_effect = assembly.clone();
        create_memo(move || {
            if !assembly_for_effect.realized.get() {
                assembly_for_effect.realize();
            }
        });
        
        assembly
    }
    
    // --- inserting elements and regulators ---
    
    // insert an element into the assembly without checking whether we already
    // have an element with the same identifier. any element that does have the
    // same identifier will get kicked out of the `elements_by_id` index
    fn insert_element_unchecked(&self, elt: impl Element + 'static) {
        // insert the element
        let id = elt.id().clone();
        let elt_rc = Rc::new(elt);
        self.elements.update(|elts| elts.insert(elt_rc.clone()));
        self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
        
        // create and insert the element's default regulators
        for reg in elt_rc.default_regulators() {
            self.insert_regulator(reg);
        }
    }
    
    pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
        let can_insert = self.elements_by_id.with_untracked(
            |elts_by_id| !elts_by_id.contains_key(elt.id())
        );
        if can_insert {
            self.insert_element_unchecked(elt);
        }
        can_insert
    }
    
    pub fn insert_element_default<T: Element + 'static>(&self) {
        // find the next unused identifier in the default sequence
        let default_id = T::default_id();
        let mut id_num = 1;
        let mut id = format!("{default_id}{id_num}");
        while self.elements_by_id.with_untracked(
            |elts_by_id| elts_by_id.contains_key(&id)
        ) {
            id_num += 1;
            id = format!("{default_id}{id_num}");
        }
        
        // create and insert the default example of `T`
        let _ = self.insert_element_unchecked(T::default(id, id_num));
    }
    
    pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
        // add the regulator to the assembly's regulator list
        self.regulators.update(
            |regs| regs.insert(regulator.clone())
        );
        
        // add the regulator to each subject's regulator list
        let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
            |subj| subj.regulators()
        ).collect();
        for regulators in subject_regulators {
            regulators.update(|regs| regs.insert(regulator.clone()));
        }
        
        // update the realization when the regulator becomes a constraint, or is
        // edited while acting as a constraint
        let self_for_effect = self.clone();
        create_effect(move || {
            /* DEBUG */
            // log the regulator update
            console_log!("Updated regulator with subjects {:?}", regulator.subjects());
            
            if regulator.try_activate() {
                self_for_effect.realized.set(false);
            }
        });
        
        /* DEBUG */
        // print an updated list of regulators
        console_log!("Regulators:");
        self.regulators.with_untracked(|regs| {
            for reg in regs.into_iter() {
                console_log!(
                    "  {:?}: {}",
                    reg.subjects(),
                    reg.set_point().with_untracked(
                        |set_pt| {
                            let spec = &set_pt.spec;
                            if spec.is_empty() {
                                "__".to_string()
                            } else {
                                spec.clone()
                            }
                        }
                    )
                );
            }
        });
    }
    
    // --- realization ---
    
    pub fn realize(&self) {
        // index the elements
        self.elements.update_silent(|elts| {
            for (index, elt) in elts.iter().enumerate() {
                elt.set_column_index(index);
            }
        });
        
        // set up the constraint problem
        let problem = self.elements.with_untracked(|elts| {
            let mut problem = ConstraintProblem::new(elts.len());
            for elt in elts {
                elt.pose(&mut problem);
            }
            self.regulators.with_untracked(|regs| {
                for reg in regs {
                    reg.pose(&mut problem);
                }
            });
            problem
        });
        
        /* DEBUG */
        // log the Gram matrix
        console_log!("Gram matrix:\n{}", problem.gram);
        
        /* DEBUG */
        // log the initial configuration matrix
        console_log!("Old configuration:{:>8.3}", problem.guess);
        
        // look for a configuration with the given Gram matrix
        let (config, tangent, success, history) = realize_gram(
            &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
        );
        
        /* DEBUG */
        // report the outcome of the search
        if success {
            console_log!("Target accuracy achieved!")
        } else {
            console_log!("Failed to reach target accuracy")
        }
        console_log!("Steps: {}", history.scaled_loss.len() - 1);
        console_log!("Loss: {}", *history.scaled_loss.last().unwrap());
        console_log!("Tangent dimension: {}", tangent.dim());
        
        if success {
            // read out the solution
            for elt in self.elements.get_clone_untracked() {
                elt.representation().update(
                    |rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
                );
            }
            
            // update the realization flag
            self.realized.set(true);
            
            // save the tangent space
            self.tangent.set_silent(tangent);
        }
    }
    
    // --- deformation ---
    
    // project the given motion to the tangent space of the solution variety and
    // move the assembly along it. the implementation is based on invariant (1)
    // from above and the following additional invariant:
    //
    //  (2) if an element is affected by a constraint, it has a column index
    //
    // we have this invariant because the assembly gets realized each time you
    // add a constraint
    pub fn deform(&self, motion: AssemblyMotion) {
        /* KLUDGE */
        // when the tangent space is zero, deformation won't do anything, but
        // the attempt to deform should be registered in the UI. this console
        // message will do for now
        if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
            console::log_1(&JsValue::from("The assembly is rigid"));
        }
        
        // give a column index to each moving element that doesn't have one yet.
        // this temporarily breaks invariant (1), but the invariant will be
        // restored when we realize the assembly at the end of the deformation.
        // in the process, we find out how many matrix columns we'll need to
        // hold the deformation
        let realized_dim = self.tangent.with(|tan| tan.assembly_dim());
        let motion_dim = {
            let mut next_column_index = realized_dim;
            for elt_motion in motion.iter() {
                let moving_elt = &elt_motion.element;
                if moving_elt.column_index().is_none() {
                    moving_elt.set_column_index(next_column_index);
                    next_column_index += 1;
                }
            }
            next_column_index
        };
        
        // project the element motions onto the tangent space of the solution
        // variety and sum them to get a deformation of the whole assembly. the
        // matrix `motion_proj` that holds the deformation has extra columns for
        // any moving elements that aren't reflected in the saved tangent space
        const ELEMENT_DIM: usize = 5;
        let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim);
        for elt_motion in motion {
            // we can unwrap the column index because we know that every moving
            // element has one at this point
            let column_index = elt_motion.element.column_index().unwrap();
            
            if column_index < realized_dim {
                // this element had a column index when we started, so by
                // invariant (1), it's reflected in the tangent space
                let mut target_columns = motion_proj.columns_mut(0, realized_dim);
                target_columns += self.tangent.with(
                    |tan| tan.proj(&elt_motion.velocity, column_index)
                );
            } else {
                // this element didn't have a column index when we started, so
                // by invariant (2), it's unconstrained
                let mut target_column = motion_proj.column_mut(column_index);
                let unif_to_std = elt_motion.element.representation().with_untracked(
                    |rep| local_unif_to_std(rep.as_view())
                );
                target_column += unif_to_std * elt_motion.velocity;
            }
        }
        
        // step the assembly along the deformation. this changes the elements'
        // normalizations, so we restore those afterward
        for elt in self.elements.get_clone_untracked() {
            elt.representation().update_silent(|rep| {
                match elt.column_index() {
                    Some(column_index) => {
                        // step the element along the deformation and then
                        // restore its normalization
                        *rep += motion_proj.column(column_index);
                        elt.project_to_normalized(rep);
                    },
                    None => {
                        console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
                    }
                };
            });
        }
        
        // bring the configuration back onto the solution variety. this also
        // gets the elements' column indices and the saved tangent space back in
        // sync
        self.realized.set(false);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    use crate::engine;
    
    #[test]
    #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
    fn unindexed_element_test() {
        let _ = create_root(|| {
            let elt = Sphere::default("sphere".to_string(), 0);
            elt.pose(&mut ConstraintProblem::new(1));
        });
    }
    
    #[test]
    #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
    fn unindexed_subject_test_inversive_distance() {
        let _ = create_root(|| {
            let subjects = [0, 1].map(
                |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc<dyn Element>
            );
            subjects[0].set_column_index(0);
            InversiveDistanceRegulator {
                subjects: subjects,
                measurement: create_memo(|| 0.0),
                set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()),
                serial: InversiveDistanceRegulator::next_serial()
            }.pose(&mut ConstraintProblem::new(2));
        });
    }
    
    #[test]
    fn curvature_drift_test() {
        const INITIAL_RADIUS: f64 = 0.25;
        let _ = create_root(|| {
            // set up an assembly containing a single sphere centered at the
            // origin
            let assembly = Assembly::new();
            let sphere_id = "sphere0";
            let _ = assembly.try_insert_element(
                // we create the sphere by hand for two reasons: to choose the
                // curvature (which can affect drift rate) and to make the test
                // independent of `Sphere::default`
                Sphere::new(
                    String::from(sphere_id),
                    String::from("Sphere 0"),
                    [0.75_f32, 0.75_f32, 0.75_f32],
                    engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS)
                )
            );
            
            // nudge the sphere repeatedly along the `z` axis
            const STEP_SIZE: f64 = 0.0025;
            const STEP_CNT: usize = 400;
            let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone());
            let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
            for _ in 0..STEP_CNT {
                assembly.deform(
                    vec![
                        ElementMotion {
                            element: sphere.clone(),
                            velocity: velocity.as_view()
                        }
                    ]
                );
            }
            
            // check how much the sphere's curvature has drifted
            const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
            const DRIFT_TOL: f64 = 0.015;
            let final_half_curv = sphere.representation().with_untracked(
                |rep| rep[Sphere::CURVATURE_COMPONENT]
            );
            assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
        });
    }
}