import { Vector3, Quaternion } from '@zephyr3d/base';
import { SpringBone } from './spring_bone.js';
import { CircularConstraint } from './spring_bone_constraint.js';

/**
 * Spring bone system manager.
 *
 * @remarks
 * Manages multiple spring bones, colliders, and constraints.
 * Updates physics simulation and applies results to bone nodes.
 *
 * @public
 */ class SpringBoneSystem {
    /** All spring bones in the system */ _springBones;
    /** All colliders in the system */ _colliders;
    /** All constraints in the system */ _constraints;
    /** Gravity vector in world space */ _gravity;
    /** Wind force vector in world space */ _windForce;
    /** Enable/disable the system */ _enabled;
    /** Number of constraint iterations per update */ _constraintIterations;
    /** Whether deltaTime is in milliseconds (true) or seconds (false) */ _deltaTimeInMilliseconds;
    /**
   * Create a spring bone system.
   *
   * @param deltaTimeInMilliseconds - Whether deltaTime is in milliseconds (default: false)
   */ constructor(deltaTimeInMilliseconds = false){
        this._springBones = [];
        this._colliders = [];
        this._constraints = [];
        this._gravity = new Vector3(0, -9.8, 0);
        this._windForce = new Vector3(0, 0, 0);
        this._enabled = true;
        this._constraintIterations = 2;
        this._deltaTimeInMilliseconds = deltaTimeInMilliseconds;
    }
    /**
   * Get/set gravity vector.
   */ get gravity() {
        return this._gravity;
    }
    set gravity(value) {
        this._gravity.set(value);
    }
    /**
   * Get/set wind force vector.
   */ get windForce() {
        return this._windForce;
    }
    set windForce(value) {
        this._windForce.set(value);
    }
    /**
   * Get/set enabled state.
   */ get enabled() {
        return this._enabled;
    }
    set enabled(value) {
        this._enabled = value;
    }
    /**
   * Add a spring bone to the system.
   *
   * @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)
   * @returns The created spring bone
   */ addSpringBone(bone, stiffness = 0.1, damping = 0.5, mass = 1, gravityScale = 1, radius = 0.05) {
        const springBone = new SpringBone(bone, stiffness, damping, mass, gravityScale, radius);
        this._springBones.push(springBone);
        return springBone;
    }
    /**
   * Add a spring bone chain to the system.
   *
   * @remarks
   * Adds multiple bones in a chain with the same parameters.
   * The first bone (root) is typically fixed and not simulated.
   *
   * @param bones - Array of bone nodes from root to tip
   * @param stiffness - Spring stiffness (0-1, default: 0.1)
   * @param damping - Damping factor (0-1, default: 0.5)
   * @param mass - Mass of each bone (default: 1)
   * @param gravityScale - Gravity scale (default: 1)
   * @param radius - Collision radius (default: 0.05)
   * @param skipRoot - Skip the first bone (root) (default: true)
   * @returns Array of created spring bones
   */ addSpringChain(bones, stiffness = 0.1, damping = 0.5, mass = 1, gravityScale = 1, radius = 0.05, skipRoot = true) {
        const springBones = [];
        const startIndex = skipRoot ? 1 : 0;
        for(let i = startIndex; i < bones.length; i++){
            const springBone = this.addSpringBone(bones[i], stiffness, damping, mass, gravityScale, radius);
            springBones.push(springBone);
        }
        return springBones;
    }
    /**
   * Remove a spring bone from the system.
   *
   * @param springBone - The spring bone to remove
   * @returns True if removed, false if not found
   */ removeSpringBone(springBone) {
        const index = this._springBones.indexOf(springBone);
        if (index >= 0) {
            this._springBones.splice(index, 1);
            return true;
        }
        return false;
    }
    /**
   * Clear all spring bones.
   */ clearSpringBones() {
        this._springBones = [];
    }
    /**
   * Add a collider to the system.
   *
   * @param collider - The collider to add
   */ addCollider(collider) {
        this._colliders.push(collider);
    }
    /**
   * Remove a collider from the system.
   *
   * @param collider - The collider to remove
   * @returns True if removed, false if not found
   */ removeCollider(collider) {
        const index = this._colliders.indexOf(collider);
        if (index >= 0) {
            this._colliders.splice(index, 1);
            return true;
        }
        return false;
    }
    /**
   * Clear all colliders.
   */ clearColliders() {
        this._colliders = [];
    }
    /**
   * Add a constraint to the system.
   *
   * @param constraint - The constraint to add
   */ addConstraint(constraint) {
        this._constraints.push(constraint);
    }
    /**
   * Remove a constraint from the system.
   *
   * @param constraint - The constraint to remove
   * @returns True if removed, false if not found
   */ removeConstraint(constraint) {
        const index = this._constraints.indexOf(constraint);
        if (index >= 0) {
            this._constraints.splice(index, 1);
            return true;
        }
        return false;
    }
    /**
   * Clear all constraints.
   */ clearConstraints() {
        this._constraints = [];
    }
    /**
   * Create circular constraints for skirt-like structures.
   *
   * @remarks
   * This is a convenience method for creating distance constraints
   * between adjacent chains arranged in a circle (e.g., skirt panels).
   *
   * @param chains - Array of spring bone chains arranged in a circle
   * @param minDistance - Minimum distance between adjacent bones (default: 0.05)
   * @param maxDistance - Maximum distance between adjacent bones (default: 0.15)
   * @param stiffness - Constraint stiffness (0-1, default: 0.5)
   * @returns The created circular constraint
   */ createCircularConstraints(chains, minDistance = 0.05, maxDistance = 0.15, stiffness = 0.5) {
        const constraint = new CircularConstraint(chains, minDistance, maxDistance, stiffness);
        this.addConstraint(constraint);
        return constraint;
    }
    /**
   * Get/set number of constraint iterations.
   *
   * @remarks
   * Higher values give more accurate constraint solving but cost more performance.
   * Typical values: 1-5
   */ get constraintIterations() {
        return this._constraintIterations;
    }
    set constraintIterations(value) {
        this._constraintIterations = Math.max(1, Math.floor(value));
    }
    /**
   * Reset all spring bones to their initial state.
   */ reset() {
        for (const springBone of this._springBones){
            springBone.reset();
        }
    }
    /**
   * Update parent bone rotation if parent is not in the spring system.
   *
   * @remarks
   * When skipRoot=true, the parent bone is not in the spring system,
   * but its rotation still needs to be updated to point toward the
   * spring bone's simulated position.
   *
   * @param springBone - The spring bone whose parent should be updated
   */ _updateParentRotation(springBone) {
        const bone = springBone.bone;
        const parent = bone.parent;
        if (!parent) {
            return; // No parent to update
        }
        // Check if parent is in the spring system
        const parentIsSpringBone = this._springBones.some((sb)=>sb.bone === parent);
        if (parentIsSpringBone) {
            return; // Parent is already being updated by the spring system
        }
        // Parent is not in the spring system, so we need to update its rotation
        // to point toward the spring bone's simulated position
        // Get parent's world position
        const parentWorldPos = new Vector3();
        parent.worldMatrix.decompose(null, null, parentWorldPos);
        // Get simulated position
        const simulatedPos = springBone.currentPosition;
        // Calculate direction from parent to simulated position (in world space)
        const currentDir = Vector3.sub(simulatedPos, parentWorldPos, new Vector3());
        const currentLength = currentDir.magnitude;
        if (currentLength < 0.000001) {
            return; // Too close to parent
        }
        currentDir.scaleBy(1 / currentLength);
        // Get grandparent (parent's parent) if it exists
        const grandparent = parent.parent;
        let grandparentWorldRot = new Quaternion().identity();
        if (grandparent) {
            grandparent.worldMatrix.decompose(null, grandparentWorldRot, null);
        }
        // Transform current direction to grandparent's local space
        const grandparentInvRot = Quaternion.conjugate(grandparentWorldRot, new Quaternion());
        const currentDirLocal = grandparentInvRot.transform(currentDir, new Vector3());
        // Original direction in local space (normalized)
        const originalDirLocal = Vector3.normalize(springBone.initialLocalPosition, new Vector3());
        // Calculate local rotation from original to current
        const localRotation = new Quaternion();
        this._fromToRotation(originalDirLocal, currentDirLocal, localRotation);
        // Apply to parent bone
        parent.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);
    }
    /**
   * Update the spring bone system.
   *
   * @param deltaTime - Time step (in seconds or milliseconds, depending on constructor parameter)
   */ update(deltaTime) {
        if (!this._enabled || deltaTime <= 0) {
            return;
        }
        // Convert to seconds if needed
        const deltaTimeSeconds = this._deltaTimeInMilliseconds ? deltaTime / 1000 : deltaTime;
        // Clamp delta time to avoid instability
        const clampedDeltaTime = Math.min(deltaTimeSeconds, 0.1);
        // Update spring bones one by one (not in parallel)
        // This ensures parent bones are updated before child bones
        for (const springBone of this._springBones){
            // Update physics
            springBone.update(clampedDeltaTime, this._gravity, this._windForce);
            // Apply collisions
            const position = springBone.currentPosition;
            const radius = springBone.radius;
            for (const collider of this._colliders){
                collider.collide(position, radius);
            }
            // Update parent bone rotation if parent is not in the spring system
            this._updateParentRotation(springBone);
            // Apply to bone node
            springBone.applyToNode();
            // Force update the bone's world matrix
            // This ensures child bones see the updated parent transform
            const bone = springBone.bone;
            if (typeof bone.updateWorldMatrix === 'function') {
                bone.updateWorldMatrix(true, false);
            }
        }
        // Apply constraints (multiple iterations for better convergence)
        for(let i = 0; i < this._constraintIterations; i++){
            for (const constraint of this._constraints){
                constraint.apply();
            }
        }
    }
    /**
   * Get all spring bones.
   */ get springBones() {
        return this._springBones;
    }
    /**
   * Get all colliders.
   */ get colliders() {
        return this._colliders;
    }
}

export { SpringBoneSystem };
//# sourceMappingURL=spring_bone_system.js.map
