import { Vector3, Quaternion } from '@zephyr3d/base';
import { IKUtils } from '../ik/ik_utils.js';
import { updateColliderFromNode, resolveSphereCollision, resolveCapsuleCollision, resolvePlaneCollision } from './spring_collider.js';

const FIXED_SIMULATION_TIME_STEP = 1 / 60;
const MAX_ACCUMULATED_SIMULATION_TIME = 1 / 20;
const MAX_SIMULATION_STEPS_PER_UPDATE = Math.max(1, Math.ceil(MAX_ACCUMULATED_SIMULATION_TIME / FIXED_SIMULATION_TIME_STEP));
const DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME = 1 / 30;
const DEFAULT_COLLIDER_SMOOTHING_TIME = 1 / 30;
/**
 * Physics engine for spring-based particle simulation
 * Uses Verlet integration and iterative constraint solving
 *
 * @public
 */ class SpringSystem {
    _chain;
    _iterations;
    _gravity;
    _wind;
    _enableInertialForces;
    _centrifugalScale;
    _coriolisScale;
    _colliders;
    _solver;
    _poseFollow;
    _maxPoseOffset;
    _poseFollowRoot;
    _poseFollowTip;
    _poseFollowExponent;
    _maxPoseOffsetRoot;
    _maxPoseOffsetTip;
    _timeAccumulator;
    _smoothedParticleTargets;
    _smoothedSphereCenters;
    _smoothedCapsuleEndpoints;
    _smoothedPlaneData;
    constructor(chain, options){
        this._chain = chain;
        this._iterations = options?.iterations ?? 5;
        this._gravity = options?.gravity?.clone() ?? new Vector3(0, -9.8, 0);
        this._wind = options?.wind?.clone() ?? new Vector3(0, 0, 0);
        this._enableInertialForces = options?.enableInertialForces ?? true;
        this._centrifugalScale = options?.centrifugalScale ?? 1.0;
        this._coriolisScale = options?.coriolisScale ?? 1.0;
        this._colliders = [];
        this._solver = options?.solver ?? 'verlet';
        this._poseFollow = Math.max(0, Math.min(1, options?.poseFollow ?? 0.35));
        this._maxPoseOffset = Math.max(0, options?.maxPoseOffset ?? 0);
        this._poseFollowRoot = Math.max(0, Math.min(1, options?.poseFollowRoot ?? this._poseFollow));
        this._poseFollowTip = Math.max(0, Math.min(1, options?.poseFollowTip ?? this._poseFollow));
        this._poseFollowExponent = Math.max(0.1, options?.poseFollowExponent ?? 1.6);
        this._maxPoseOffsetRoot = Math.max(0, options?.maxPoseOffsetRoot ?? this._maxPoseOffset);
        this._maxPoseOffsetTip = Math.max(0, options?.maxPoseOffsetTip ?? this._maxPoseOffset);
        this._timeAccumulator = 0;
        this._smoothedParticleTargets = new WeakMap();
        this._smoothedSphereCenters = new WeakMap();
        this._smoothedCapsuleEndpoints = new WeakMap();
        this._smoothedPlaneData = new WeakMap();
    }
    /**
   * Updates the physics simulation
   * @param deltaTime - Time step in seconds
   */ update(deltaTime) {
        const frameDt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
        if (frameDt <= 0) {
            return;
        }
        this._timeAccumulator = Math.min(this._timeAccumulator + frameDt, MAX_ACCUMULATED_SIMULATION_TIME);
        const stepCount = Math.min(MAX_SIMULATION_STEPS_PER_UPDATE, Math.floor((this._timeAccumulator + 1e-8) / FIXED_SIMULATION_TIME_STEP));
        if (stepCount <= 0) {
            return;
        }
        this._timeAccumulator = Math.max(0, this._timeAccumulator - stepCount * FIXED_SIMULATION_TIME_STEP);
        for(let i = 0; i < stepCount; i++){
            this.simulateStep(FIXED_SIMULATION_TIME_STEP, i === 0 ? frameDt : 0);
        }
    }
    simulateStep(dt, inputDeltaTime) {
        // Step 1: Save all particle positions before updating
        if (this._enableInertialForces) {
            for (const p of this._chain.particles){
                p.lastFramePosition.set(p.position);
            }
        }
        // Step 2: Update fixed particles from their scene nodes
        this.updateFixedParticles(inputDeltaTime);
        // Step 3: Calculate global rotation parameters
        let rotationCenter = null;
        let angularVelocity = null;
        if (this._enableInertialForces && dt > 0.0001) {
            const result = this.calculateGlobalRotation(dt);
            rotationCenter = result.center;
            angularVelocity = result.omega;
        }
        // Step 4: Verlet integration with inertial forces
        for(let i = 0; i < this._chain.particles.length; i++){
            const p = this._chain.particles[i];
            if (p.fixed) {
                continue;
            }
            // Calculate velocity (implicit in Verlet)
            const velocity = Vector3.sub(p.position, p.prevPosition, new Vector3());
            velocity.scaleBy(p.damping);
            // Apply external forces (gravity + wind)
            const acceleration = Vector3.add(this._gravity, this._wind, new Vector3());
            // Apply inertial forces from rotating reference frame
            if (this._enableInertialForces && rotationCenter && angularVelocity) {
                const inertialAccel = this.calculateInertialAcceleration(p, rotationCenter, angularVelocity, velocity, this._centrifugalScale, this._coriolisScale);
                Vector3.add(acceleration, inertialAccel, acceleration);
            }
            const positionDelta = Vector3.scale(acceleration, dt * dt, new Vector3());
            Vector3.add(velocity, positionDelta, velocity);
            // Update position
            p.prevPosition.set(p.position);
            Vector3.add(p.position, velocity, p.position);
        }
        // Step 5: Iteratively solve constraints
        if (this._solver === 'xpbd') {
            // Reset Lagrange multipliers at the start of each time step
            for (const constraint of this._chain.constraints){
                constraint.lambda = 0;
            }
        }
        for(let iter = 0; iter < this._iterations; iter++){
            for (const constraint of this._chain.constraints){
                if (this._solver === 'xpbd') {
                    this.solveConstraintXPBD(constraint, dt);
                } else {
                    this.solveConstraint(constraint);
                }
            }
            // Pull particles back toward the animated pose to preserve hair silhouette.
            // Normalize follow strength across solver iterations so tuning stays intuitive.
            this.solvePosePreservation(this._iterations);
            // Apply collision constraints
            this.solveCollisions(inputDeltaTime);
        }
    }
    /**
   * Updates fixed particles to match their scene node positions
   */ updateFixedParticles(deltaTime) {
        const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME);
        for (const particle of this._chain.particles){
            if (!particle.node) {
                continue;
            }
            const worldMatrix = particle.node.worldMatrix;
            const worldPos = new Vector3(worldMatrix.m03, worldMatrix.m13, worldMatrix.m23);
            const smoothedTarget = this.getSmoothedParticleTarget(particle, worldPos, blend);
            particle.animPosition.set(smoothedTarget);
            if (particle.fixed) {
                particle.position.set(smoothedTarget);
                particle.prevPosition.set(smoothedTarget);
                // Maintain position history for rotation center estimation
                if (this._enableInertialForces) {
                    if (!particle.positionHistory) {
                        particle.positionHistory = [];
                    }
                    particle.positionHistory.push(smoothedTarget.clone());
                    // Keep only last 5 frames
                    if (particle.positionHistory.length > 5) {
                        particle.positionHistory.shift();
                    }
                }
            }
        }
    }
    /**
   * Solves pose preservation (long-range attachment to animated pose).
   * This keeps strands close to authored shape while preserving dynamic movement.
   */ solvePosePreservation(totalIterations) {
        if (this._poseFollowRoot <= 0 && this._poseFollowTip <= 0) {
            return;
        }
        const lastIndex = Math.max(1, this._chain.particles.length - 1);
        for(let i = 0; i < this._chain.particles.length; i++){
            const particle = this._chain.particles[i];
            if (particle.fixed) {
                continue;
            }
            const t = Math.pow(i / lastIndex, this._poseFollowExponent);
            const particlePoseFollow = this.lerp(this._poseFollowRoot, this._poseFollowTip, t);
            // Convert user-facing per-frame follow into per-iteration follow:
            // effective = 1 - (1 - follow)^iterations
            const iterationFollow = totalIterations > 1 ? 1 - Math.pow(Math.max(0, 1 - particlePoseFollow), 1 / totalIterations) : particlePoseFollow;
            const toAnim = Vector3.sub(particle.animPosition, particle.position, new Vector3());
            const correction = Vector3.scale(toAnim, iterationFollow, new Vector3());
            Vector3.add(particle.position, correction, particle.position);
            const particleMaxPoseOffset = this.lerp(this._maxPoseOffsetRoot, this._maxPoseOffsetTip, t);
            if (particleMaxPoseOffset > 0) {
                const offset = Vector3.sub(particle.position, particle.animPosition, new Vector3());
                const offsetLen = offset.magnitude;
                if (offsetLen > particleMaxPoseOffset && offsetLen > 1e-6) {
                    offset.scaleBy(particleMaxPoseOffset / offsetLen);
                    Vector3.add(particle.animPosition, offset, particle.position);
                }
            }
        }
    }
    lerp(a, b, t) {
        return a + (b - a) * t;
    }
    /**
   * Calculates global rotation parameters from fixed particle movements
   * Uses position history to estimate rotation center
   */ calculateGlobalRotation(dt) {
        // Collect fixed particles with movement
        const fixedParticles = [];
        const velocities = [];
        for (const p of this._chain.particles){
            if (!p.fixed) {
                continue;
            }
            const velocity = Vector3.sub(p.position, p.lastFramePosition, new Vector3());
            velocity.scaleBy(1.0 / dt);
            if (velocity.magnitudeSq > 0.001) {
                fixedParticles.push(p);
                velocities.push(velocity);
            }
        }
        if (fixedParticles.length === 0) {
            return {
                center: new Vector3(0, 0, 0),
                omega: new Vector3(0, 0, 0)
            };
        }
        // Estimate rotation center using position history
        let center;
        if (fixedParticles.length === 1) {
            // Single fixed particle: use position history to estimate rotation center
            center = this.estimateRotationCenterFromHistory(fixedParticles[0], velocities[0]);
        } else {
            // Multiple fixed particles: use their average position
            center = new Vector3(0, 0, 0);
            for (const p of fixedParticles){
                Vector3.add(center, p.position, center);
            }
            center.scaleBy(1.0 / fixedParticles.length);
        }
        // Estimate angular velocity
        let sumOmega = new Vector3(0, 0, 0);
        let count = 0;
        for(let i = 0; i < fixedParticles.length; i++){
            const r = Vector3.sub(fixedParticles[i].position, center, new Vector3());
            const v = velocities[i];
            const rLengthSq = r.magnitudeSq;
            if (rLengthSq > 0.0001) {
                // ω = (r × v) / |r|²
                const omega = Vector3.cross(r, v, new Vector3());
                omega.scaleBy(1.0 / rLengthSq);
                Vector3.add(sumOmega, omega, sumOmega);
                count++;
            }
        }
        if (count > 0) {
            sumOmega.scaleBy(1.0 / count);
        }
        return {
            center,
            omega: sumOmega
        };
    }
    /**
   * Estimates rotation center from a single particle's position history
   * Uses circular motion fitting
   */ estimateRotationCenterFromHistory(particle, currentVelocity) {
        const history = particle.positionHistory;
        if (!history || history.length < 3) {
            // Not enough history: estimate using velocity perpendicular direction
            // Assume rotation center is perpendicular to velocity
            // Use a default radius based on velocity magnitude
            const speed = currentVelocity.magnitude;
            if (speed < 0.001) {
                return particle.position.clone();
            }
            // Estimate radius: for typical character rotation, assume ~0.5-1.0m radius
            const estimatedRadius = Math.max(0.5, speed * 0.5);
            // Direction perpendicular to velocity (in the plane of motion)
            // We need to guess which perpendicular direction - use cross product with up vector
            const up = new Vector3(0, 1, 0);
            const perpDir = Vector3.cross(currentVelocity, up, new Vector3());
            if (perpDir.magnitudeSq < 0.0001) {
                // Velocity is vertical, use another perpendicular
                perpDir.set(new Vector3(1, 0, 0));
            } else {
                perpDir.inplaceNormalize();
            }
            // Rotation center is perpendicular to velocity
            const center = Vector3.add(particle.position, Vector3.scale(perpDir, estimatedRadius, new Vector3()), new Vector3());
            return center;
        }
        // Fit a circle through the last 3 positions
        // Use positions at indices: 0 (oldest), middle, last (newest)
        const p1 = history[0];
        const p2 = history[Math.floor(history.length / 2)];
        const p3 = history[history.length - 1];
        // Calculate circle center from 3 points
        const center = this.calculateCircleCenter(p1, p2, p3);
        return center;
    }
    /**
   * Calculates the center of a circle passing through 3 points
   * Uses perpendicular bisector method
   */ calculateCircleCenter(p1, p2, p3) {
        // Midpoints
        const mid12 = Vector3.scale(Vector3.add(p1, p2, new Vector3()), 0.5, new Vector3());
        const mid23 = Vector3.scale(Vector3.add(p2, p3, new Vector3()), 0.5, new Vector3());
        // Direction vectors
        const dir12 = Vector3.sub(p2, p1, new Vector3());
        const dir23 = Vector3.sub(p3, p2, new Vector3());
        // Normal vectors (perpendicular bisectors)
        // For 3D, we need to find perpendiculars in the plane of the three points
        const normal = Vector3.cross(dir12, dir23, new Vector3());
        if (normal.magnitudeSq < 0.0001) {
            // Points are collinear, return midpoint
            return Vector3.scale(Vector3.add(Vector3.add(p1, p2, new Vector3()), p3, new Vector3()), 1.0 / 3.0, new Vector3());
        }
        normal.inplaceNormalize();
        // Perpendicular to dir12 in the plane
        const perp12 = Vector3.cross(dir12, normal, new Vector3()).inplaceNormalize();
        // Perpendicular to dir23 in the plane
        const perp23 = Vector3.cross(dir23, normal, new Vector3()).inplaceNormalize();
        // Find intersection of two lines:
        // Line 1: mid12 + t * perp12
        // Line 2: mid23 + s * perp23
        // Solve: mid12 + t * perp12 = mid23 + s * perp23
        const diff = Vector3.sub(mid23, mid12, new Vector3());
        // Use 2D projection for simplicity (project onto plane perpendicular to normal)
        // Solve in the plane: t * perp12 - s * perp23 = diff
        // Use least squares or pick the dominant components
        const det = perp12.x * perp23.y - perp12.y * perp23.x;
        if (Math.abs(det) > 0.0001) {
            const t = (diff.x * perp23.y - diff.y * perp23.x) / det;
            const center = Vector3.add(mid12, Vector3.scale(perp12, t, new Vector3()), new Vector3());
            return center;
        }
        // Fallback: use centroid
        return Vector3.scale(Vector3.add(Vector3.add(p1, p2, new Vector3()), p3, new Vector3()), 1.0 / 3.0, new Vector3());
    }
    /**
   * Calculates inertial acceleration for a particle in a rotating reference frame
   */ calculateInertialAcceleration(particle, rotationCenter, angularVelocity, particleVelocity, centrifugalScale, coriolisScale) {
        // Vector from rotation center to particle
        const r = Vector3.sub(particle.position, rotationCenter, new Vector3());
        // Centrifugal acceleration: a_centrifugal = ω × (ω × r)
        const omegaCrossR = Vector3.cross(angularVelocity, r, new Vector3());
        const centrifugalAccel = Vector3.cross(angularVelocity, omegaCrossR, new Vector3());
        centrifugalAccel.scaleBy(centrifugalScale);
        // Coriolis acceleration: a_coriolis = -2ω × v_relative
        // v_relative is the particle's velocity in the rotating frame
        // Use the velocity passed in (already calculated from prevPosition)
        const coriolisAccel = Vector3.cross(angularVelocity, particleVelocity, new Vector3());
        coriolisAccel.scaleBy(-2 * coriolisScale);
        // Total inertial acceleration
        const totalAccel = Vector3.add(centrifugalAccel, coriolisAccel, new Vector3());
        return totalAccel;
    }
    /**
   * Solves a single spring constraint using XPBD (Extended Position-Based Dynamics).
   *
   * Reference: Müller et al., "Detailed Rigid Body Simulation with Extended Position Based Dynamics", 2020.
   */ solveConstraintXPBD(constraint, dt) {
        const pA = this._chain.particles[constraint.particleA];
        const pB = this._chain.particles[constraint.particleB];
        const wA = pA.fixed ? 0 : 1.0 / pA.mass;
        const wB = pB.fixed ? 0 : 1.0 / pB.mass;
        const wSum = wA + wB;
        if (wSum < 1e-10) {
            return;
        }
        const delta = Vector3.sub(pB.position, pA.position, new Vector3());
        const currentLength = delta.magnitude;
        if (currentLength < 0.0001) {
            return;
        }
        // Constraint value C = currentLength - restLength
        const C = currentLength - constraint.restLength;
        // Scaled compliance: alphaTilde = compliance / dt^2
        const alphaTilde = constraint.compliance / (dt * dt);
        // XPBD Lagrange multiplier update
        const deltaLambda = (-C - alphaTilde * constraint.lambda) / (wSum + alphaTilde);
        constraint.lambda += deltaLambda;
        // Correction direction (unit vector from A to B)
        const n = Vector3.scale(delta, 1.0 / currentLength, new Vector3());
        if (!pA.fixed) {
            // deltaX_a = -w_a * deltaLambda * n
            Vector3.add(pA.position, Vector3.scale(n, -wA * deltaLambda, new Vector3()), pA.position);
        }
        if (!pB.fixed) {
            // deltaX_b = +w_b * deltaLambda * n
            Vector3.add(pB.position, Vector3.scale(n, wB * deltaLambda, new Vector3()), pB.position);
        }
    }
    /**
   * Solves a single spring constraint (Verlet / PBD)
   */ solveConstraint(constraint) {
        const pA = this._chain.particles[constraint.particleA];
        const pB = this._chain.particles[constraint.particleB];
        // Calculate current distance
        const delta = Vector3.sub(pB.position, pA.position, new Vector3());
        const currentLength = delta.magnitude;
        if (currentLength < 0.0001) {
            return;
        } // Avoid division by zero
        // Calculate correction
        const diff = (currentLength - constraint.restLength) / currentLength;
        const correction = Vector3.scale(delta, diff * constraint.stiffness * 0.5, new Vector3());
        // Apply correction (considering mass and fixed state)
        if (!pA.fixed) {
            Vector3.add(pA.position, correction, pA.position);
        }
        if (!pB.fixed) {
            Vector3.sub(pB.position, correction, pB.position);
        }
    }
    /**
   * Solves collisions for all particles
   */ solveCollisions(deltaTime) {
        // Update dynamic colliders from their nodes
        const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_COLLIDER_SMOOTHING_TIME);
        const spheres = [];
        const capsules = [];
        const planes = [];
        for (const collider of this._colliders){
            if (collider.node) {
                updateColliderFromNode(collider);
            }
            if (!collider.enabled) {
                continue;
            }
            switch(collider.type){
                case 'sphere':
                    {
                        const source = collider;
                        spheres.push({
                            particleCollider: source,
                            collider: {
                                ...source,
                                center: this.getSmoothedSphereCenter(source, blend)
                            }
                        });
                        break;
                    }
                case 'capsule':
                    {
                        const source = collider;
                        const endpoints = this.getSmoothedCapsuleEndpoints(source, blend);
                        capsules.push({
                            particleCollider: source,
                            collider: {
                                ...source,
                                start: endpoints.start,
                                end: endpoints.end
                            }
                        });
                        break;
                    }
                case 'plane':
                    {
                        const source = collider;
                        const plane = this.getSmoothedPlaneData(source, blend);
                        planes.push({
                            particleCollider: source,
                            collider: {
                                ...source,
                                point: plane.point,
                                normal: plane.normal
                            }
                        });
                        break;
                    }
            }
        }
        // Check each particle against all colliders
        for (const particle of this._chain.particles){
            if (particle.fixed) {
                continue; // Skip fixed particles
            }
            for (const collider of spheres){
                resolveSphereCollision(particle.position, collider.collider);
            }
            for (const collider of capsules){
                resolveCapsuleCollision(particle.position, collider.collider);
            }
            for (const collider of planes){
                resolvePlaneCollision(particle.position, collider.collider);
            }
        }
    }
    /**
   * Applies simulation results to scene nodes
   * @param weight - Blend weight [0-1] (default: 1.0)
   */ applyToNodes(weight = 1.0) {
        for(let i = 0; i < this._chain.particles.length - 1; i++){
            const particle = this._chain.particles[i];
            const nextParticle = this._chain.particles[i + 1];
            const node = particle.node;
            // Skip if no node
            if (!node) {
                continue;
            }
            // Get current bone direction from node's world matrix (before physics)
            // This reflects the current animation/skeleton state
            const currentBonePos = new Vector3(node.worldMatrix.m03, node.worldMatrix.m13, node.worldMatrix.m23);
            const nextNode = nextParticle.node;
            if (!nextNode) {
                continue;
            }
            const nextBonePos = new Vector3(nextNode.worldMatrix.m03, nextNode.worldMatrix.m13, nextNode.worldMatrix.m23);
            const originalDir = Vector3.sub(nextBonePos, currentBonePos, new Vector3());
            // Get current bone rotation from node's world matrix
            const currentBoneRotation = new Quaternion();
            node.worldMatrix.decompose(null, currentBoneRotation, null);
            // Calculate new direction from physics simulation
            const newDir = Vector3.sub(nextParticle.position, particle.position, new Vector3());
            // Calculate rotation needed to align original direction to new direction
            const deltaRotation = new Quaternion();
            IKUtils.fromToRotation(originalDir, newDir, deltaRotation);
            // Calculate new world rotation
            let worldRotation = Quaternion.multiply(deltaRotation, currentBoneRotation, new Quaternion());
            // Blend with current rotation based on weight
            if (weight < 1) {
                Quaternion.slerp(currentBoneRotation, worldRotation, weight, worldRotation);
            }
            // Convert world rotation to local rotation (relative to parent)
            const parent = node.parent;
            if (parent) {
                const parentWorldRotation = new Quaternion();
                parent.worldMatrix.decompose(null, parentWorldRotation, null);
                // localRotation = conjugate(parentWorldRotation) * worldRotation
                const parentInvRotation = Quaternion.conjugate(parentWorldRotation, new Quaternion());
                const localRotation = Quaternion.multiply(parentInvRotation, worldRotation, new Quaternion());
                node.rotation = localRotation;
            } else {
                // Root node has no parent, world rotation is local rotation
                node.rotation = worldRotation;
            }
        }
    }
    /**
   * Resets the simulation to initial state
   */ reset() {
        this._chain.reset();
        for (const particle of this._chain.particles){
            particle.animPosition.set(particle.originalPosition);
            particle.lastFramePosition.set(particle.originalPosition);
            if (particle.positionHistory) {
                particle.positionHistory.length = 0;
            }
        }
        this._timeAccumulator = 0;
        this._smoothedParticleTargets = new WeakMap();
        this._smoothedSphereCenters = new WeakMap();
        this._smoothedCapsuleEndpoints = new WeakMap();
        this._smoothedPlaneData = new WeakMap();
    }
    /**
   * Gets the spring chain
   */ get chain() {
        return this._chain;
    }
    /**
   * Gets the current gravity
   */ get gravity() {
        return this._gravity;
    }
    set gravity(gravity) {
        this._gravity.set(gravity);
    }
    /**
   * Gets the current wind
   */ get wind() {
        return this._wind;
    }
    set wind(wind) {
        this._wind.set(wind);
    }
    /**
   * Gets the number of iterations
   */ get iterations() {
        return this._iterations;
    }
    set iterations(count) {
        this._iterations = Math.max(1, count);
    }
    /**
   * Gets whether inertial forces are enabled
   */ get enableInertialForces() {
        return this._enableInertialForces;
    }
    set enableInertialForces(enabled) {
        this._enableInertialForces = enabled;
    }
    /**
   * Gets the centrifugal force scale
   */ get centrifugalScale() {
        return this._centrifugalScale;
    }
    set centrifugalScale(scale) {
        this._centrifugalScale = Math.max(0, scale);
    }
    /**
   * Gets the Coriolis force scale
   */ get coriolisScale() {
        return this._coriolisScale;
    }
    set coriolisScale(scale) {
        this._coriolisScale = Math.max(0, scale);
    }
    /**
   * Gets the constraint solver type
   */ get solver() {
        return this._solver;
    }
    set solver(type) {
        if (this._solver !== type) {
            this._solver = type;
            if (type === 'xpbd') {
                for (const c of this._chain.constraints){
                    c.lambda = 0;
                }
            }
        }
    }
    /**
   * Gets pose preservation strength [0-1]
   */ get poseFollow() {
        return this._poseFollow;
    }
    set poseFollow(value) {
        const v = Math.max(0, Math.min(1, value));
        this._poseFollow = v;
        this._poseFollowRoot = v;
        this._poseFollowTip = v;
    }
    /**
   * Gets max allowed deviation from animated pose
   */ get maxPoseOffset() {
        return this._maxPoseOffset;
    }
    set maxPoseOffset(value) {
        const v = Math.max(0, value);
        this._maxPoseOffset = v;
        this._maxPoseOffsetRoot = v;
        this._maxPoseOffsetTip = v;
    }
    /**
   * Gets root pose follow strength [0-1]
   */ get poseFollowRoot() {
        return this._poseFollowRoot;
    }
    set poseFollowRoot(value) {
        this._poseFollowRoot = Math.max(0, Math.min(1, value));
    }
    /**
   * Gets tip pose follow strength [0-1]
   */ get poseFollowTip() {
        return this._poseFollowTip;
    }
    set poseFollowTip(value) {
        this._poseFollowTip = Math.max(0, Math.min(1, value));
    }
    /**
   * Gets exponent for root-to-tip interpolation
   */ get poseFollowExponent() {
        return this._poseFollowExponent;
    }
    set poseFollowExponent(value) {
        this._poseFollowExponent = Math.max(0.1, value);
    }
    /**
   * Gets root max allowed deviation from animated pose
   */ get maxPoseOffsetRoot() {
        return this._maxPoseOffsetRoot;
    }
    set maxPoseOffsetRoot(value) {
        this._maxPoseOffsetRoot = Math.max(0, value);
    }
    /**
   * Gets tip max allowed deviation from animated pose
   */ get maxPoseOffsetTip() {
        return this._maxPoseOffsetTip;
    }
    set maxPoseOffsetTip(value) {
        this._maxPoseOffsetTip = Math.max(0, value);
    }
    /**
   * Adds a collider to the system
   */ addCollider(collider) {
        this._colliders.push(collider);
        this._smoothedSphereCenters = new WeakMap();
        this._smoothedCapsuleEndpoints = new WeakMap();
        this._smoothedPlaneData = new WeakMap();
    }
    /**
   * Removes a collider from the system
   */ removeCollider(collider) {
        const index = this._colliders.indexOf(collider);
        if (index >= 0) {
            this._colliders.splice(index, 1);
            this._smoothedSphereCenters = new WeakMap();
            this._smoothedCapsuleEndpoints = new WeakMap();
            this._smoothedPlaneData = new WeakMap();
            return true;
        }
        return false;
    }
    /**
   * Clears all colliders
   */ clearColliders() {
        this._colliders = [];
        this._smoothedSphereCenters = new WeakMap();
        this._smoothedCapsuleEndpoints = new WeakMap();
        this._smoothedPlaneData = new WeakMap();
    }
    /**
   * Gets all colliders
   */ get colliders() {
        return this._colliders;
    }
    getTemporalBlendFactor(deltaTime, smoothingTime) {
        const dt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
        if (smoothingTime <= 0) {
            return 1;
        }
        if (dt <= 0) {
            return 0;
        }
        return 1 - Math.exp(-dt / smoothingTime);
    }
    getSmoothedParticleTarget(target, current, blend) {
        const cached = this._smoothedParticleTargets.get(target);
        if (!cached || blend >= 1) {
            const next = current.clone();
            this._smoothedParticleTargets.set(target, next);
            return next;
        }
        cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
        return cached;
    }
    getSmoothedSphereCenter(collider, blend) {
        const current = collider.center?.clone() ?? new Vector3();
        const cached = this._smoothedSphereCenters.get(collider);
        if (!cached || blend >= 1) {
            this._smoothedSphereCenters.set(collider, current);
            return current;
        }
        cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
        return cached;
    }
    getSmoothedCapsuleEndpoints(collider, blend) {
        const currentStart = collider.start?.clone() ?? new Vector3();
        const currentEnd = collider.end?.clone() ?? new Vector3();
        const cached = this._smoothedCapsuleEndpoints.get(collider);
        if (!cached || blend >= 1) {
            const next = {
                start: currentStart,
                end: currentEnd
            };
            this._smoothedCapsuleEndpoints.set(collider, next);
            return next;
        }
        cached.start.setXYZ(cached.start.x + (currentStart.x - cached.start.x) * blend, cached.start.y + (currentStart.y - cached.start.y) * blend, cached.start.z + (currentStart.z - cached.start.z) * blend);
        cached.end.setXYZ(cached.end.x + (currentEnd.x - cached.end.x) * blend, cached.end.y + (currentEnd.y - cached.end.y) * blend, cached.end.z + (currentEnd.z - cached.end.z) * blend);
        return cached;
    }
    getSmoothedPlaneData(collider, blend) {
        const currentPoint = collider.point?.clone() ?? new Vector3();
        const currentNormal = collider.normal?.clone() ?? Vector3.axisPY();
        if (currentNormal.magnitudeSq > 1e-8) {
            currentNormal.inplaceNormalize();
        } else {
            currentNormal.setXYZ(0, 1, 0);
        }
        const cached = this._smoothedPlaneData.get(collider);
        if (!cached || blend >= 1) {
            const next = {
                point: currentPoint,
                normal: currentNormal
            };
            this._smoothedPlaneData.set(collider, next);
            return next;
        }
        cached.point.setXYZ(cached.point.x + (currentPoint.x - cached.point.x) * blend, cached.point.y + (currentPoint.y - cached.point.y) * blend, cached.point.z + (currentPoint.z - cached.point.z) * blend);
        cached.normal.setXYZ(cached.normal.x + (currentNormal.x - cached.normal.x) * blend, cached.normal.y + (currentNormal.y - cached.normal.y) * blend, cached.normal.z + (currentNormal.z - cached.normal.z) * blend);
        if (cached.normal.magnitudeSq > 1e-8) {
            cached.normal.inplaceNormalize();
        } else {
            cached.normal.setXYZ(0, 1, 0);
        }
        return cached;
    }
}

export { SpringSystem };
//# sourceMappingURL=spring_system.js.map
