import { Vector3, Quaternion } from '@zephyr3d/base';

/**
 * Spring bone for physics-based animation.
 *
 * @remarks
 * A spring bone simulates physics on a bone node, commonly used for:
 * - Skirts and dresses
 * - Hair strands
 * - Tails
 * - Capes and cloth
 *
 * The bone follows its parent's animation but adds secondary motion
 * based on physics simulation (springs, gravity, inertia).
 *
 * @public
 */ class SpringBone {
    /** The bone node this spring controls */ _bone;
    /** Current position in world space */ _currentPosition;
    /** Previous position in world space (for velocity calculation) */ _previousPosition;
    /** Initial local position relative to parent */ _initialLocalPosition;
    /** Initial bone length (distance from parent) */ _boneLength;
    /** Spring stiffness (0-1, higher = stiffer) */ _stiffness;
    /** Damping factor (0-1, higher = more damping) */ _damping;
    /** Mass of the bone (affects inertia) */ _mass;
    /** Gravity scale (1 = normal gravity, 0 = no gravity) */ _gravityScale;
    /** Radius for collision detection */ _radius;
    /**
   * Create a spring bone.
   *
   * @param bone - The bone node to control
   * @param stiffness - Spring stiffness (0-1, default: 0.1)
   * @param damping - Damping factor (0-1, default: 0.5)
   * @param mass - Mass of the bone (default: 1)
   * @param gravityScale - Gravity scale (default: 1)
   * @param radius - Collision radius (default: 0.05)
   */ constructor(bone, stiffness = 0.1, damping = 0.5, mass = 1, gravityScale = 1, radius = 0.05){
        this._bone = bone;
        this._stiffness = Math.max(0, Math.min(1, stiffness));
        this._damping = Math.max(0, Math.min(1, damping));
        this._mass = Math.max(0.01, mass);
        this._gravityScale = gravityScale;
        this._radius = Math.max(0, radius);
        // Store initial local transform
        this._initialLocalPosition = bone.position.clone();
        this._boneLength = this._initialLocalPosition.magnitude;
        // Initialize positions in world space
        const worldPos = new Vector3();
        bone.worldMatrix.decompose(null, null, worldPos);
        this._currentPosition = worldPos.clone();
        this._previousPosition = worldPos.clone();
    }
    /**
   * Get the bone node.
   */ get bone() {
        return this._bone;
    }
    /**
   * Get/set spring stiffness (0-1).
   */ get stiffness() {
        return this._stiffness;
    }
    set stiffness(value) {
        this._stiffness = Math.max(0, Math.min(1, value));
    }
    /**
   * Get/set damping factor (0-1).
   */ get damping() {
        return this._damping;
    }
    set damping(value) {
        this._damping = Math.max(0, Math.min(1, value));
    }
    /**
   * Get/set mass.
   */ get mass() {
        return this._mass;
    }
    set mass(value) {
        this._mass = Math.max(0.01, value);
    }
    /**
   * Get/set gravity scale.
   */ get gravityScale() {
        return this._gravityScale;
    }
    set gravityScale(value) {
        this._gravityScale = value;
    }
    /**
   * Get/set collision radius.
   */ get radius() {
        return this._radius;
    }
    set radius(value) {
        this._radius = Math.max(0, value);
    }
    /**
   * Get current position in world space.
   */ get currentPosition() {
        return this._currentPosition;
    }
    /**
   * Get initial local position relative to parent.
   */ get initialLocalPosition() {
        return this._initialLocalPosition;
    }
    /**
   * Reset the spring bone to its initial state.
   */ reset() {
        const worldPos = new Vector3();
        this._bone.worldMatrix.decompose(null, null, worldPos);
        this._currentPosition.set(worldPos);
        this._previousPosition.set(worldPos);
    }
    /**
   * Update spring physics.
   *
   * @param deltaTime - Time step in seconds
   * @param gravity - Gravity vector in world space
   * @param windForce - Wind force vector in world space
   */ update(deltaTime, gravity, windForce) {
        // Clamp delta time to avoid instability (1ms to 100ms)
        if (deltaTime <= 0.001 || deltaTime > 0.1) {
            return; // Skip invalid or too large time steps
        }
        // Get parent's world position
        const parent = this._bone.parent;
        if (!parent) {
            return; // Root bone, no spring simulation
        }
        const parentWorldPos = new Vector3();
        parent.worldMatrix.decompose(null, null, parentWorldPos);
        // Calculate velocity from previous frame
        const velocity = Vector3.sub(this._currentPosition, this._previousPosition, new Vector3());
        Vector3.scale(velocity, 1 / deltaTime, velocity);
        // Store current position as previous
        this._previousPosition.set(this._currentPosition);
        // Calculate forces
        const totalForce = new Vector3();
        // 1. Gravity force
        const gravityForce = Vector3.scale(gravity, this._gravityScale * this._mass, new Vector3());
        totalForce.addBy(gravityForce);
        // 2. Wind force
        totalForce.addBy(windForce);
        // 3. Damping force (opposes velocity)
        const dampingForce = Vector3.scale(velocity, -this._damping * this._mass, new Vector3());
        totalForce.addBy(dampingForce);
        // 4. Spring force (only if stretched beyond bone length)
        const toChild = Vector3.sub(this._currentPosition, parentWorldPos, new Vector3());
        const currentLength = toChild.magnitude;
        if (currentLength > this._boneLength + 0.001) {
            // Bone is stretched, apply restoring force
            const stretchAmount = currentLength - this._boneLength;
            const direction = Vector3.scale(toChild, -1 / currentLength, new Vector3());
            const springForce = Vector3.scale(direction, stretchAmount * this._stiffness * 100, new Vector3());
            totalForce.addBy(springForce);
        }
        // Calculate acceleration (F = ma, a = F/m)
        const acceleration = Vector3.scale(totalForce, 1 / this._mass, new Vector3());
        // Update velocity
        velocity.addBy(Vector3.scale(acceleration, deltaTime, new Vector3()));
        // Apply velocity damping (additional damping to prevent oscillation)
        const velocityMagnitude = velocity.magnitude;
        if (velocityMagnitude > 0.001) {
            const dampingFactor = Math.max(0, 1 - this._damping * deltaTime * 10);
            velocity.scaleBy(dampingFactor);
        }
        // Update position
        this._currentPosition.addBy(Vector3.scale(velocity, deltaTime, new Vector3()));
        // Constrain to bone length (hard constraint)
        const finalToChild = Vector3.sub(this._currentPosition, parentWorldPos, new Vector3());
        const finalLength = finalToChild.magnitude;
        if (finalLength > 0.000001) {
            const direction = Vector3.scale(finalToChild, 1 / finalLength, new Vector3());
            const targetPos = Vector3.add(parentWorldPos, Vector3.scale(direction, this._boneLength, new Vector3()), new Vector3());
            // Soft constraint: blend toward target position
            // This prevents the hard snap that causes oscillation
            const constraintStrength = 0.5; // 0 = no constraint, 1 = hard constraint
            this._currentPosition.x += (targetPos.x - this._currentPosition.x) * constraintStrength;
            this._currentPosition.y += (targetPos.y - this._currentPosition.y) * constraintStrength;
            this._currentPosition.z += (targetPos.z - this._currentPosition.z) * constraintStrength;
        }
    }
    /**
   * Apply the simulated position to the bone node.
   *
   * @remarks
   * This converts the world space position back to local rotation.
   */ applyToNode() {
        const parent = this._bone.parent;
        if (!parent) {
            return;
        }
        // Get parent's world position and rotation
        const parentWorldPos = new Vector3();
        const parentWorldRot = new Quaternion();
        parent.worldMatrix.decompose(null, parentWorldRot, parentWorldPos);
        // Calculate current direction from parent to simulated position (in world space)
        const currentDir = Vector3.sub(this._currentPosition, parentWorldPos, new Vector3());
        const currentLength = currentDir.magnitude;
        if (currentLength < 0.000001) {
            return; // Too close to parent
        }
        currentDir.scaleBy(1 / currentLength);
        // Transform current direction to parent's local space
        const parentInvRot = Quaternion.conjugate(parentWorldRot, new Quaternion());
        const currentDirLocal = parentInvRot.transform(currentDir, new Vector3());
        // Original direction in local space (normalized)
        const originalDirLocal = Vector3.normalize(this._initialLocalPosition, new Vector3());
        // Calculate local rotation from original to current
        const localRotation = new Quaternion();
        this._fromToRotation(originalDirLocal, currentDirLocal, localRotation);
        // Apply to bone
        this._bone.rotation = localRotation;
    }
    /**
   * Calculate rotation from one direction to another.
   */ _fromToRotation(from, to, result) {
        const dot = Vector3.dot(from, to);
        if (dot >= 0.999999) {
            return result.identity();
        }
        if (dot <= -0.999999) {
            let axis = Vector3.cross(Vector3.axisPX(), from, new Vector3());
            const lenSq = axis.x * axis.x + axis.y * axis.y + axis.z * axis.z;
            if (lenSq < 0.000001) {
                axis = Vector3.cross(Vector3.axisPY(), from, new Vector3());
            }
            axis.inplaceNormalize();
            return result.fromAxisAngle(axis, Math.PI);
        }
        const axis = Vector3.cross(from, to).inplaceNormalize();
        const angle = Math.acos(dot);
        return result.fromAxisAngle(axis, angle);
    }
}

export { SpringBone };
//# sourceMappingURL=spring_bone.js.map
