import { Disposable, weightedAverage, Quaternion, Interpolator, Vector3 } from '@zephyr3d/base';
import { AnimationClip } from './animation.js';
import { NodeRotationTrack } from './rotationtrack.js';
import { NodeEulerRotationTrack } from './eulerrotationtrack.js';
import { NodeTranslationTrack } from './translationtrack.js';
import { NodeScaleTrack } from './scaletrack.js';
import { Skeleton, HumanoidBodyRig } from './skeleton.js';

function cloneInterpolator(src) {
    return new Interpolator(src.mode, src.target, src.inputs instanceof Float32Array ? new Float32Array(src.inputs) : [
        ...src.inputs
    ], src.outputs instanceof Float32Array ? new Float32Array(src.outputs) : [
        ...src.outputs
    ]);
}
function createJointRetargetRemap(srcSkeleton, dstSkeleton, srcJoint, dstJoint, translationRotation) {
    const si = srcSkeleton.joints.indexOf(srcJoint);
    const di = dstSkeleton.joints.indexOf(dstJoint);
    const srcBindPose = srcSkeleton.bindPose[si];
    const dstBindPose = dstSkeleton.bindPose[di];
    const srcLen = srcBindPose.position.magnitude;
    const dstLen = dstBindPose.position.magnitude;
    return {
        dstNode: dstJoint,
        dstJointIndex: di,
        srcNode: srcJoint,
        srcBindRotInv: Quaternion.inverse(srcBindPose.rotation),
        dstBindRot: dstBindPose.rotation.clone(),
        srcBindPos: srcBindPose.position.clone(),
        dstBindPos: dstBindPose.position.clone(),
        translationScale: srcLen > 1e-6 ? dstLen / srcLen : 1,
        translationRotation
    };
}
function retargetRotation(qSrcAnim, remap, out) {
    Quaternion.multiply(remap.srcBindRotInv, qSrcAnim, out);
    Quaternion.multiply(remap.dstBindRot, out, out);
    return out.inplaceNormalize();
}
function retargetRotationTangent(qSrcTangent, remap, out) {
    Quaternion.multiply(remap.srcBindRotInv, qSrcTangent, out);
    Quaternion.multiply(remap.dstBindRot, out, out);
    return out;
}
function retargetTranslationValue(srcValue, remap, out) {
    out.setXYZ(srcValue.x, srcValue.y, srcValue.z);
    out.subBy(remap.srcBindPos);
    out.scaleBy(remap.translationScale);
    if (remap.translationRotation) {
        remap.translationRotation.transform(out, out);
    }
    out.addBy(remap.dstBindPos);
    return out;
}
function retargetTranslationTangent(srcValue, remap, out) {
    out.setXYZ(srcValue.x, srcValue.y, srcValue.z);
    out.scaleBy(remap.translationScale);
    if (remap.translationRotation) {
        remap.translationRotation.transform(out, out);
    }
    return out;
}
function retargetRotationTrack(src, remap) {
    const isCubic = src.interpolator.mode === 'cubicspline';
    const frameStride = isCubic ? 12 : 4;
    const numFrames = src.interpolator.inputs.length;
    const srcOutputs = src.interpolator.outputs;
    const newOutputs = new Float32Array(srcOutputs.length);
    const t = isCubic ? 4 : 0;
    const q = new Quaternion();
    for(let f = 0; f < numFrames; f++){
        const base = f * frameStride;
        q.setXYZW(srcOutputs[base + t], srcOutputs[base + t + 1], srcOutputs[base + t + 2], srcOutputs[base + t + 3]);
        retargetRotation(q, remap, q);
        if (isCubic) {
            // Cubic quaternion tangents live in the same component space, so apply the
            // same constant bind-pose transform without normalizing them.
            q.setXYZW(srcOutputs[base], srcOutputs[base + 1], srcOutputs[base + 2], srcOutputs[base + 3]);
            newOutputs.set(retargetRotationTangent(q, remap, q), base);
            newOutputs.set(retargetRotation(new Quaternion(srcOutputs.subarray(base + 4, base + 8)), remap, q), base + 4);
            q.setXYZW(srcOutputs[base + 8], srcOutputs[base + 9], srcOutputs[base + 10], srcOutputs[base + 11]);
            newOutputs.set(retargetRotationTangent(q, remap, q), base + 8);
        } else {
            newOutputs.set(q, base);
        }
    }
    return new NodeRotationTrack(new Interpolator(src.interpolator.mode, 'quat', new Float32Array(src.interpolator.inputs), newOutputs));
}
function retargetEulerToRotationTrack(src, remap) {
    const srcInputs = src.interpolator.inputs;
    const srcOutputs = src.interpolator.outputs;
    const isCubic = src.interpolator.mode === 'cubicspline';
    const frameStride = isCubic ? 9 : 3;
    const t = isCubic ? 3 : 0;
    const numFrames = srcInputs.length;
    const newOutputs = new Float32Array(numFrames * 4);
    const q = new Quaternion();
    for(let f = 0; f < numFrames; f++){
        const base = f * frameStride + t;
        q.fromEulerAngle(srcOutputs[base], srcOutputs[base + 1], srcOutputs[base + 2]);
        retargetRotation(q, remap, q);
        newOutputs.set(q, f * 4);
    }
    return new NodeRotationTrack(new Interpolator(src.interpolator.mode === 'step' ? 'step' : 'linear', 'quat', new Float32Array(srcInputs), newOutputs));
}
function retargetTranslationTrack(src, remap) {
    const isCubic = src.interpolator.mode === 'cubicspline';
    const frameStride = isCubic ? 9 : 3;
    const numFrames = src.interpolator.inputs.length;
    const srcOutputs = src.interpolator.outputs;
    const newOutputs = new Float32Array(srcOutputs.length);
    const t = isCubic ? 3 : 0;
    const v = new Vector3();
    for(let f = 0; f < numFrames; f++){
        const base = f * frameStride;
        v.setXYZ(srcOutputs[base + t], srcOutputs[base + t + 1], srcOutputs[base + t + 2]);
        retargetTranslationValue(v, remap, v);
        if (isCubic) {
            v.setXYZ(srcOutputs[base], srcOutputs[base + 1], srcOutputs[base + 2]);
            newOutputs.set(retargetTranslationTangent(v, remap, v), base);
            v.setXYZ(srcOutputs[base + 3], srcOutputs[base + 4], srcOutputs[base + 5]);
            newOutputs.set(retargetTranslationValue(v, remap, v), base + 3);
            v.setXYZ(srcOutputs[base + 6], srcOutputs[base + 7], srcOutputs[base + 8]);
            newOutputs.set(retargetTranslationTangent(v, remap, v), base + 6);
        } else {
            newOutputs.set(v, base);
        }
    }
    return new NodeTranslationTrack(new Interpolator(src.interpolator.mode, 'vec3', new Float32Array(src.interpolator.inputs), newOutputs));
}
function findRotationTrack(tracks) {
    return tracks?.find((track)=>track instanceof NodeRotationTrack || track instanceof NodeEulerRotationTrack) ?? null;
}
function collectTrackTimes(track, times) {
    if (track instanceof NodeRotationTrack || track instanceof NodeEulerRotationTrack) {
        for (const time of track.interpolator.inputs){
            times.add(time);
        }
    }
}
function sampleRotationTrack(track, time, fallback, out) {
    if (!track) {
        out.set(fallback);
        return out;
    }
    const state = track.calculateState({}, time);
    out.set(state);
    return out;
}
function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRootRotation, dstRootRotation, dstClip, remaps) {
    const rotationTracksByRemap = new Map();
    const times = new Set([
        0,
        sourceClip.timeDuration
    ]);
    const remapsByDstNode = new Map();
    const srcJointSet = new Set(srcSkeleton.joints);
    const dstJointSet = new Set(dstSkeleton.joints);
    const srcBindRotByNode = new Map();
    const dstBindRotByNode = new Map();
    const srcRotationTrackByNode = new Map();
    for(let i = 0; i < srcSkeleton.joints.length; i++){
        const joint = srcSkeleton.joints[i];
        srcBindRotByNode.set(joint, srcSkeleton.bindPose[i].rotation);
        const track = findRotationTrack(sourceClip.tracks.get(joint));
        srcRotationTrackByNode.set(joint, track);
        collectTrackTimes(track, times);
    }
    for(let i = 0; i < dstSkeleton.joints.length; i++){
        dstBindRotByNode.set(dstSkeleton.joints[i], dstSkeleton.bindPose[i].rotation);
    }
    for (const remap of remaps){
        const track = findRotationTrack(sourceClip.tracks.get(remap.srcNode));
        rotationTracksByRemap.set(remap, track);
        remapsByDstNode.set(remap.dstNode, remap);
    }
    const inputs = new Float32Array([
        ...times
    ].sort((a, b)=>a - b));
    const srcBindWorldRots = new Map();
    const dstBindWorldRots = new Map();
    const srcAnimWorldRots = new Map();
    const dstAnimWorldRots = new Map();
    const outputsByRemap = new Map();
    const tmpLocalRot = new Quaternion();
    const tmpWorldDelta = new Quaternion();
    const tmpParentInv = new Quaternion();
    for (const remap of remaps){
        outputsByRemap.set(remap, new Float32Array(inputs.length * 4));
    }
    function getSrcBindWorldRot(node) {
        let rot = srcBindWorldRots.get(node);
        if (!rot) {
            rot = (srcBindRotByNode.get(node) ?? Quaternion.identity()).clone();
            const parent = node.parent;
            if (parent && srcJointSet.has(parent)) {
                Quaternion.multiply(getSrcBindWorldRot(parent), rot, rot);
            } else {
                Quaternion.multiply(srcRootRotation, rot, rot);
            }
            srcBindWorldRots.set(node, rot);
        }
        return rot;
    }
    function getDstBindWorldRot(node) {
        let rot = dstBindWorldRots.get(node);
        if (!rot) {
            rot = (dstBindRotByNode.get(node) ?? Quaternion.identity()).clone();
            const parent = node.parent;
            if (parent && dstJointSet.has(parent)) {
                Quaternion.multiply(getDstBindWorldRot(parent), rot, rot);
            } else {
                Quaternion.multiply(dstRootRotation, rot, rot);
            }
            dstBindWorldRots.set(node, rot);
        }
        return rot;
    }
    function getSrcAnimWorldRot(node, time) {
        let rot = srcAnimWorldRots.get(node);
        if (!rot) {
            rot = new Quaternion();
            srcAnimWorldRots.set(node, rot);
        }
        const parent = node.parent;
        const parentWorldRot = parent && srcJointSet.has(parent) ? getSrcAnimWorldRot(parent, time) : null;
        sampleRotationTrack(srcRotationTrackByNode.get(node) ?? null, time, srcBindRotByNode.get(node) ?? Quaternion.identity(), tmpLocalRot);
        if (parentWorldRot) {
            Quaternion.multiply(parentWorldRot, tmpLocalRot, rot);
        } else {
            Quaternion.multiply(srcRootRotation, tmpLocalRot, rot);
        }
        return rot;
    }
    function getDstAnimWorldRot(remap, time) {
        let rot = dstAnimWorldRots.get(remap.dstNode);
        if (!rot) {
            rot = new Quaternion();
            dstAnimWorldRots.set(remap.dstNode, rot);
        }
        const srcBindWorldRot = getSrcBindWorldRot(remap.srcNode);
        const srcAnimWorldRot = getSrcAnimWorldRot(remap.srcNode, time);
        Quaternion.multiply(srcAnimWorldRot, Quaternion.inverse(srcBindWorldRot, tmpWorldDelta), tmpWorldDelta);
        Quaternion.multiply(tmpWorldDelta, getDstBindWorldRot(remap.dstNode), rot);
        return rot;
    }
    function getDstParentAnimWorldRot(node, time) {
        const parent = node.parent;
        if (!parent || !dstJointSet.has(parent)) {
            return dstRootRotation;
        }
        const mappedParent = remapsByDstNode.get(parent);
        if (mappedParent) {
            return getDstAnimWorldRot(mappedParent, time);
        }
        let rot = dstAnimWorldRots.get(parent);
        if (!rot) {
            rot = (dstBindRotByNode.get(parent) ?? Quaternion.identity()).clone();
            const parentWorldRot = getDstParentAnimWorldRot(parent, time);
            if (parentWorldRot) {
                Quaternion.multiply(parentWorldRot, rot, rot);
            }
            dstAnimWorldRots.set(parent, rot);
        }
        return rot;
    }
    for(let i = 0; i < inputs.length; i++){
        const time = inputs[i];
        srcAnimWorldRots.clear();
        dstAnimWorldRots.clear();
        for (const remap of remaps){
            const dstAnimWorldRot = getDstAnimWorldRot(remap, time);
            const parentWorldRot = getDstParentAnimWorldRot(remap.dstNode, time);
            if (parentWorldRot) {
                Quaternion.multiply(Quaternion.inverse(parentWorldRot, tmpParentInv), dstAnimWorldRot, tmpLocalRot);
            } else {
                tmpLocalRot.set(dstAnimWorldRot);
            }
            tmpLocalRot.inplaceNormalize();
            outputsByRemap.get(remap).set(tmpLocalRot, i * 4);
        }
    }
    for (const remap of remaps){
        if (!rotationTracksByRemap.get(remap)) {
            continue;
        }
        const outputs = new Float32Array(outputsByRemap.get(remap));
        const track = new NodeRotationTrack(new Interpolator('linear', 'quat', new Float32Array(inputs), outputs));
        track.name = 'rotation';
        track.target = remap.dstNode.persistentId;
        track.jointIndex = remap.dstJointIndex;
        dstClip.addTrack(remap.dstNode, track);
    }
}
/**
 * Animation set
 *
 * Manages a collection of named animation clips for a model and orchestrates:
 * - Playback state (time, loops, speed, weights, fade-in/out).
 * - Blending across multiple tracks targeting the same property via weighted averages.
 * - Skeleton usage and application for clips that drive skeletal animation.
 * - Active track registration and cleanup as clips start/stop.
 *
 * Usage:
 * - Create or retrieve `AnimationClip`s by name.
 * - Start playback with `playAnimation(name, options)`.
 * - Advance animation with `update(deltaSeconds)`.
 * - Optionally adjust weight while playing with `setAnimationWeight(name, weight)`.
 *
 * Lifetime:
 * - Disposing the set releases references to the model, clips, and clears active state.
 *
 * @public
 */ class AnimationSet extends Disposable {
    /** @internal */ _model;
    /** @internal */ _animations;
    /** @internal */ _skeletons;
    /** @internal */ _activeTracks;
    /** @internal */ _activeSkeletons;
    /** @internal */ _activeAnimations;
    /**
   * Create an AnimationSet controlling the provided model.
   *
   * @param model - The SceneNode (model root) controlled by this animation set.
   */ constructor(model){
        super();
        this._model = model;
        this._animations = {};
        this._activeTracks = new Map();
        this._activeSkeletons = new Map();
        this._activeAnimations = new Map();
        this._skeletons = [];
    }
    /**
   * The model (SceneNode) controlled by this animation set.
   */ get model() {
        return this._model;
    }
    /**
   * Number of animation clips registered in this set.
   */ get numAnimations() {
        return Object.getOwnPropertyNames(this._animations).length;
    }
    /**
   * The skeletons used by animations in this set.
   */ get skeletons() {
        return this._skeletons;
    }
    /**
   * Retrieve an animation clip by name.
   *
   * @param name - Name of the animation.
   * @returns The clip if present; otherwise null.
   */ get(name) {
        return this._animations[name] ?? null;
    }
    /**
   * Create and register a new animation clip.
   *
   * @param name - Unique name for the animation clip.
   * @param embedded - Whether the clip is embedded/owned (implementation-specific). Default false.
   * @returns The created clip, or null if the name is empty or not unique.
   */ createAnimation(name, embedded = false) {
        if (!name || this._animations[name]) {
            console.error('Animation must have unique name');
            return null;
        } else {
            const animation = new AnimationClip(name, this, embedded);
            this._animations[name] = animation;
            this._model.scene?.queueUpdateNode(this._model);
            return animation;
        }
    }
    /**
   * Delete and dispose an animation clip by name.
   *
   * - If the animation is currently playing, it is first stopped (immediately).
   *
   * @param name - Name of the animation to remove.
   */ deleteAnimation(name) {
        const animation = this._animations[name];
        if (animation) {
            this.stopAnimation(name);
            delete this._animations[name];
            animation.dispose();
        }
    }
    /**
   * Get the list of all registered animation names.
   *
   * @returns An array of clip names.
   */ getAnimationNames() {
        return Object.keys(this._animations);
    }
    /**
   * Advance and apply active animations.
   *
   * Responsibilities per call:
   * - Update time cursor for each active clip (respecting speedRatio and looping).
   * - Enforce repeat limits and apply fade-out termination if configured.
   * - For each animated target, blend active tracks (weighted by clip weight × fade-in × fade-out)
   *   and apply the resulting state to the target.
   * - Apply all active skeletons to update skinning transforms.
   *
   * @param deltaInSeconds - Time step in seconds since last update.
   */ update(deltaInSeconds) {
        this._activeAnimations.forEach((v, k)=>{
            if (v.fadeOut > 0 && v.fadeOutStart < 0) {
                v.fadeOutStart = v.animateTime;
            }
            // Update animation time
            if (v.firstFrame) {
                v.firstFrame = false;
            } else {
                const timeAdvance = deltaInSeconds * v.speedRatio;
                v.currentTime += timeAdvance;
                v.animateTime += timeAdvance;
                if (v.currentTime > k.timeDuration) {
                    v.repeatCounter++;
                    v.currentTime = 0;
                } else if (v.currentTime < 0) {
                    v.repeatCounter++;
                    v.currentTime = k.timeDuration;
                }
                if (v.repeat !== 0 && v.repeatCounter >= v.repeat) {
                    this.stopAnimation(k.name);
                } else if (v.fadeOut > 0) {
                    if (v.animateTime - v.fadeOutStart >= v.fadeOut) {
                        this.stopAnimation(k.name);
                    }
                }
            }
        });
        // Update tracks
        this._activeTracks.forEach((v, k)=>{
            v.forEach((alltracks)=>{
                // Only deal with tracks which have not been removed
                const tracks = alltracks.filter((track)=>track.animation && this.isPlayingAnimation(track.animation.name));
                if (tracks.length > 0) {
                    const weights = tracks.map((track)=>{
                        const info = this._activeAnimations.get(track.animation);
                        const weight = info.weight;
                        const fadeIn = info.fadeIn === 0 ? 1 : Math.min(1, info.animateTime / info.fadeIn);
                        let fadeOut = 1;
                        if (info.fadeOut !== 0) {
                            fadeOut = 1 - (info.animateTime - info.fadeOutStart) / info.fadeOut;
                        }
                        return weight * fadeIn * fadeOut;
                    });
                    const states = tracks.map((track)=>{
                        const info = this._activeAnimations.get(track.animation);
                        const t = info.currentTime / track.animation.timeDuration * track.getDuration();
                        return track.calculateState(k, t);
                    });
                    const state = weightedAverage(weights, states, (a, b, t)=>{
                        return tracks[0].mixState(a, b, t);
                    });
                    tracks[0].applyState(k, state);
                }
            });
        });
        // Update skeletons
        this._skeletons.forEach((v)=>{
            v.get()?.apply(deltaInSeconds);
        });
    }
    /**
   * Check whether an animation is currently playing.
   *
   * @param name - Optional animation name. If omitted, returns true if any animation is playing.
   * @returns True if playing; otherwise false.
   */ isPlayingAnimation(name) {
        if (name) {
            const animation = this._animations[name];
            return !!animation && this._activeAnimations.has(animation);
        }
        return this._activeAnimations.size > 0;
    }
    /**
   * Get an animation clip by name.
   *
   * Alias of `get(name)` returning a nullable type.
   *
   * @param name - Name of the animation.
   * @returns The clip if present; otherwise null.
   */ getAnimationClip(name) {
        return this._animations[name] ?? null;
    }
    /**
   * Set the runtime blend weight for a currently playing animation.
   *
   * Has no effect if the clip is not active.
   *
   * @param name - Name of the playing animation.
   * @param weight - New weight value used during blending.
   */ setAnimationWeight(name, weight) {
        const ani = this._animations[name];
        if (!ani) {
            console.error(`Animation ${name} not exists`);
            return;
        }
        const info = this._activeAnimations.get(ani);
        if (info) {
            info.weight = weight;
        }
    }
    /**
   * Start (or update) playback of an animation clip.
   *
   * Behavior:
   * - If the clip is already playing, updates its fade-in (resets fade-out).
   * - Otherwise initializes playback state (repeat counter, speed, weight, initial time).
   * - Registers clip tracks and skeletons into the active sets for blending and application.
   *
   * @param name - Name of the animation to play.
   * @param options - Playback options (repeat, speedRatio, fadeIn).
   */ playAnimation(name, options) {
        const ani = this._animations[name];
        if (!ani) {
            console.error(`Animation ${name} not exists`);
            return;
        }
        const fadeIn = Math.max(options?.fadeIn ?? 0, 0);
        const info = this._activeAnimations.get(ani);
        if (info) {
            info.fadeOut = 0;
            info.fadeIn = fadeIn;
        } else {
            const repeat = options?.repeat ?? 0;
            const speedRatio = options?.speedRatio ?? 1;
            const weight = options?.weight ?? ani.weight ?? 1;
            this._activeAnimations.set(ani, {
                repeat,
                weight,
                speedRatio,
                fadeIn,
                fadeOut: 0,
                repeatCounter: 0,
                currentTime: speedRatio < 0 ? ani.timeDuration : 0,
                animateTime: 0,
                fadeOutStart: 0,
                firstFrame: true
            });
            ani.tracks?.forEach((v, k)=>{
                let nodeTracks = this._activeTracks.get(k);
                if (!nodeTracks) {
                    nodeTracks = new Map();
                    this._activeTracks.set(k, nodeTracks);
                }
                for (const track of v){
                    const blendId = track.getBlendId();
                    let blendedTracks = nodeTracks.get(blendId);
                    if (!blendedTracks) {
                        blendedTracks = [];
                        nodeTracks.set(blendId, blendedTracks);
                    }
                    blendedTracks.push(track);
                }
            });
            ani.skeletons?.forEach((v, k)=>{
                const skeleton = this.model.findSkeletonById(k);
                if (skeleton) {
                    const refcount = this._activeSkeletons.get(skeleton);
                    if (refcount) {
                        this._activeSkeletons.set(skeleton, refcount + 1);
                    } else {
                        this._activeSkeletons.set(skeleton, 1);
                    }
                    skeleton.playing = true;
                }
            });
        }
    }
    /**
   * Stop playback of an animation clip.
   *
   * Behavior:
   * - If `options.fadeOut > 0`, marks the clip for fade-out; actual removal occurs after fade completes.
   * - If `fadeOut` is 0 or omitted, immediately:
   *   - Removes the clip from active animations.
   *   - Unregisters its tracks from active track maps.
   *   - Decrements skeleton reference counts; resets and removes skeletons when refcount reaches 0.
   *
   * @param name - Name of the animation to stop.
   * @param options - Optional fade-out configuration.
   */ stopAnimation(name, options) {
        const ani = this._animations[name];
        if (!ani) {
            console.error(`Animation ${name} not exists`);
            return;
        }
        const info = this._activeAnimations.get(ani);
        if (info) {
            const fadeOut = Math.max(options?.fadeOut ?? 0, 0);
            if (fadeOut !== 0) {
                info.fadeOut = fadeOut;
                info.fadeOutStart = -1;
            } else {
                this._activeAnimations.delete(ani);
                this._activeTracks.forEach((v)=>{
                    v.forEach((tracks, id)=>{
                        v.set(id, tracks.filter((track)=>track.animation !== ani));
                    });
                });
                ani.skeletons?.forEach((v, k)=>{
                    const skeleton = this.model.findSkeletonById(k);
                    if (skeleton) {
                        const refcount = this._activeSkeletons.get(skeleton);
                        if (refcount === 1) {
                            skeleton.reset();
                            this._activeSkeletons.delete(skeleton);
                        } else {
                            this._activeSkeletons.set(skeleton, refcount - 1);
                        }
                    }
                });
            }
        }
    }
    /**
   * Copy a humanoid animation clip from another AnimationSet into this one via humanoid rig mapping.
   *
   * Prerequisites:
   * - Both source and destination skeletons must have a non-null `humanoidJointMapping`.
   * - Joints are matched by shared `HumanoidBodyRig` / `HumanoidHandRig` keys instead of joint names.
   * - The source clip must exist in `sourceSet` and must be driven by exactly one skeleton.
   *
   * @param sourceSet - The AnimationSet to copy from.
   * @param animationName - Name of the clip to copy.
   * @param targetName - Name for the new clip in this set. Defaults to `animationName`.
   * @returns The newly created AnimationClip, or null on failure.
   */ copyHumanoidAnimationFrom(sourceSet, animationName, targetName) {
        const destName = targetName ?? animationName;
        const sourceClip = sourceSet.get(animationName);
        if (!sourceClip) {
            console.error(`copyHumanoidAnimationFrom: animation '${animationName}' not found in source set`);
            return null;
        }
        if (this._animations[destName]) {
            console.error(`copyHumanoidAnimationFrom: animation '${destName}' already exists in target set`);
            return null;
        }
        if (sourceClip.skeletons.size !== 1) {
            console.error(`copyHumanoidAnimationFrom: source animation clip must be affected by exactly one skeleton`);
            return null;
        }
        const srcSkeletonId = [
            ...sourceClip.skeletons
        ][0];
        const srcSkeleton = Skeleton.findSkeletonById(srcSkeletonId);
        if (!srcSkeleton) {
            console.error(`copyHumanoidAnimationFrom: source skeleton '${srcSkeletonId}' not found`);
            return null;
        }
        // Check that source skeleton has humanoid mapping
        const srcHumanoidMapping = srcSkeleton.humanoidJointMapping;
        if (!srcHumanoidMapping) {
            console.error(`copyHumanoidAnimationFrom: source skeleton does not have a humanoid joint mapping`);
            return null;
        }
        const nodeMap = new Map();
        const jointRemapBySrcNode = new Map();
        // Find a destination skeleton that has a humanoid mapping and share at least the body rig keys
        let srcJointsFiltered = [];
        let dstJointsFiltered = [];
        const dstSkeleton = this._skeletons.map((ref)=>ref.get()).find((sk)=>{
            if (!sk) {
                return false;
            }
            // Check that destination skeleton has humanoid mapping
            const dstHumanoidMapping = sk.humanoidJointMapping;
            if (!dstHumanoidMapping) {
                return false;
            }
            // Collect matched (srcJoint, dstJoint) pairs via shared humanoid rig keys
            const srcMatched = [];
            const dstMatched = [];
            // Match body rig joints
            for (const key of Object.keys(srcHumanoidMapping.body)){
                const srcJoint = srcHumanoidMapping.body[key];
                const dstJoint = dstHumanoidMapping.body[key];
                if (srcJoint && dstJoint) {
                    srcMatched.push(srcJoint);
                    dstMatched.push(dstJoint);
                }
            }
            // Match left hand rig joints only when both sides define them; skip silently if either is absent
            if (srcHumanoidMapping.leftHand && dstHumanoidMapping.leftHand) {
                for (const key of Object.keys(srcHumanoidMapping.leftHand)){
                    const srcJoint = srcHumanoidMapping.leftHand[key];
                    const dstJoint = dstHumanoidMapping.leftHand[key];
                    if (srcJoint && dstJoint) {
                        srcMatched.push(srcJoint);
                        dstMatched.push(dstJoint);
                    }
                }
            }
            // Match right hand rig joints only when both sides define them; skip silently if either is absent
            if (srcHumanoidMapping.rightHand && dstHumanoidMapping.rightHand) {
                for (const key of Object.keys(srcHumanoidMapping.rightHand)){
                    const srcJoint = srcHumanoidMapping.rightHand[key];
                    const dstJoint = dstHumanoidMapping.rightHand[key];
                    if (srcJoint && dstJoint) {
                        srcMatched.push(srcJoint);
                        dstMatched.push(dstJoint);
                    }
                }
            }
            if (srcMatched.length === 0) {
                return false;
            }
            srcJointsFiltered = srcMatched;
            dstJointsFiltered = dstMatched;
            return true;
        });
        if (!dstSkeleton) {
            console.error(`copyHumanoidAnimationFrom: no matching humanoid skeleton in target set for '${srcSkeletonId}'`);
            return null;
        }
        // Compute the world-space rotation of the parent chain above the Hips joint,
        // walking ALL ancestor nodes up to (but not including) the model root.
        // This must include non-joint nodes such as "Armature" in Mixamo rigs, which
        // carry a 90° X rotation that is NOT captured by humanoidRootRotation (which
        // only traverses nodes inside the skeleton joints list).
        function computeHipsParentChainRotation(hipsNode, modelRoot, skeleton) {
            const result = Quaternion.identity();
            const jointSet = new Set(skeleton.joints);
            let p = hipsNode.parent;
            while(p && p !== modelRoot){
                if (!jointSet.has(p)) {
                    Quaternion.multiply(p.rotation, result, result);
                }
                p = p.parent;
            }
            return result;
        }
        const srcHipsNode = srcSkeleton.humanoidJointMapping.body[HumanoidBodyRig.Hips];
        const dstHipsNode = dstSkeleton.humanoidJointMapping.body[HumanoidBodyRig.Hips];
        const srcRootRot = computeHipsParentChainRotation(srcHipsNode, sourceSet.model, srcSkeleton);
        const dstRootRot = computeHipsParentChainRotation(dstHipsNode, this._model, dstSkeleton);
        const hipsTranslationRotation = Quaternion.multiply(Quaternion.inverse(dstRootRot), srcRootRot);
        const jointRemaps = [];
        const mappedSrcNodes = new Set();
        const mappedDstNodes = new Set();
        // Build remap for matched joint pairs
        for(let fi = 0; fi < srcJointsFiltered.length; fi++){
            const srcJoint = srcJointsFiltered[fi];
            const dstJoint = dstJointsFiltered[fi];
            if (mappedSrcNodes.has(srcJoint) || mappedDstNodes.has(dstJoint)) {
                continue;
            }
            mappedSrcNodes.add(srcJoint);
            mappedDstNodes.add(dstJoint);
            const isHipsBone = srcJoint === srcHipsNode;
            nodeMap.set(srcJoint, dstJoint);
            const remap = createJointRetargetRemap(srcSkeleton, dstSkeleton, srcJoint, dstJoint, isHipsBone ? hipsTranslationRotation : undefined);
            jointRemaps.push(remap);
            jointRemapBySrcNode.set(srcJoint, remap);
        }
        const dstClip = this.createAnimation(destName);
        if (!dstClip) {
            return null;
        }
        dstClip.timeDuration = sourceClip.timeDuration;
        dstClip.weight = sourceClip.weight;
        dstClip.autoPlay = sourceClip.autoPlay;
        // Register destination skeleton
        dstClip.addSkeleton(dstSkeleton.persistentId);
        bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRootRot, dstRootRot, dstClip, jointRemaps);
        for (const srcNode of srcJointsFiltered){
            const srcTracks = sourceClip.tracks.get(srcNode);
            if (!srcTracks) {
                continue;
            }
            const dstNode = nodeMap.get(srcNode);
            const remap = jointRemapBySrcNode.get(srcNode);
            for (const srcTrack of srcTracks){
                let dstTrack;
                if (srcTrack instanceof NodeRotationTrack) {
                    continue;
                } else if (srcTrack instanceof NodeEulerRotationTrack) {
                    continue;
                } else if (srcTrack instanceof NodeTranslationTrack) {
                    dstTrack = retargetTranslationTrack(srcTrack, remap);
                } else if (srcTrack instanceof NodeScaleTrack) {
                    dstTrack = new NodeScaleTrack(cloneInterpolator(srcTrack.interpolator));
                } else {
                    console.warn(`copyHumanoidAnimationFrom: unsupported track type '${srcTrack.constructor.name}', skipping`);
                    continue;
                }
                dstTrack.name = srcTrack.name;
                dstTrack.target = dstNode.persistentId;
                dstTrack.jointIndex = remap.dstJointIndex;
                dstClip.addTrack(dstNode, dstTrack);
            }
        }
        return dstClip;
    }
    /**
   * Copy an animation clip from another AnimationSet into this one.
   *
   * Prerequisites:
   * - Both sets must reference skeletons with identical joint names and counts.
   * - The source clip must exist in `sourceSet`.
   *
   * @param sourceSet - The AnimationSet to copy from.
   * @param animationName - Name of the clip to copy.
   * @param targetName - Name for the new clip in this set. Defaults to `animationName`.
   * @param excludeJoint - Optional predicate; joints whose name returns true are excluded from
   *   skeleton structure matching.
   * @returns The newly created AnimationClip, or null on failure.
   *
   * @deprecated Use the new {@link AnimationSet.copyHumanoidAnimationFrom} method instead.
   */ copyAnimationFrom(sourceSet, animationName, targetName, excludeJoint) {
        const destName = targetName ?? animationName;
        const sourceClip = sourceSet.get(animationName);
        if (!sourceClip) {
            console.error(`copyAnimationFrom: animation '${animationName}' not found in source set`);
            return null;
        }
        if (this._animations[destName]) {
            console.error(`copyAnimationFrom: animation '${destName}' already exists in target set`);
            return null;
        }
        if (sourceClip.skeletons.size !== 1) {
            console.error(`copyAnimationFrom: source animation clip must be affected by exactly one skeleton`);
            return null;
        }
        const srcSkeletonId = [
            ...sourceClip.skeletons
        ][0];
        const srcSkeleton = Skeleton.findSkeletonById(srcSkeletonId);
        if (!srcSkeleton) {
            console.error(`copyAnimationFrom: source skeleton '${srcSkeletonId}' not found`);
            return null;
        }
        const nodeMap = new Map();
        const jointRemapBySrcNode = new Map();
        const jointsFiltered = srcSkeleton.joints.filter((j)=>!excludeJoint?.(j.name));
        const srcRootNode = findRootJoint(jointsFiltered);
        if (!srcRootNode) {
            console.error(`copyAnimationFrom: cannot determine the root joint for source skeleton`);
            return null;
        }
        // Build filtered joint lists for matching (exclude joints rejected by filterJoint)
        const srcJointsFiltered = sortJoints(srcRootNode, jointsFiltered);
        if (!srcJointsFiltered) {
            console.error(`copyAnimationFrom: invalid source skeleton structure`);
            return null;
        }
        let dstJointsFiltered = [];
        const dstSkeleton = this._skeletons.map((ref)=>ref.get()).find((sk)=>{
            if (!sk) {
                return false;
            }
            const jointsFiltered = sk.joints.filter((j)=>!excludeJoint?.(j.name));
            if (jointsFiltered.length !== srcJointsFiltered.length) {
                return false;
            }
            const rootNode = findRootJoint(jointsFiltered);
            if (!rootNode) {
                return false;
            }
            const sortedJointsFiltered = sortJoints(rootNode, jointsFiltered);
            if (sortedJointsFiltered && srcJointsFiltered.every((j, i)=>j.name === sortedJointsFiltered[i].name)) {
                dstJointsFiltered = sortedJointsFiltered;
                return true;
            }
            return false;
        });
        if (!dstJointsFiltered || !dstSkeleton) {
            console.error(`copyAnimationFrom: no matching skeleton in target set for '${srcSkeletonId}'`);
            return null;
        }
        // Build remap only for joints that pass the filter
        for(let fi = 0; fi < srcJointsFiltered.length; fi++){
            const srcJoint = srcJointsFiltered[fi];
            const dstJoint = dstJointsFiltered[fi];
            nodeMap.set(srcJoint, dstJoint);
            jointRemapBySrcNode.set(srcJoint, createJointRetargetRemap(srcSkeleton, dstSkeleton, srcJoint, dstJoint));
        }
        const dstClip = this.createAnimation(destName);
        if (!dstClip) {
            return null;
        }
        dstClip.timeDuration = sourceClip.timeDuration;
        dstClip.weight = sourceClip.weight;
        dstClip.autoPlay = sourceClip.autoPlay;
        // Register destination skeleton
        dstClip.addSkeleton(dstSkeleton.persistentId);
        for (const srcNode of srcJointsFiltered){
            const srcTracks = sourceClip.tracks.get(srcNode);
            if (!srcTracks) {
                console.error(`copyAnimationFrom: no track for joint: ${srcNode.name}`);
                return null;
            }
            const dstNode = nodeMap.get(srcNode);
            const remap = jointRemapBySrcNode.get(srcNode);
            for (const srcTrack of srcTracks){
                let dstTrack;
                if (srcTrack instanceof NodeRotationTrack) {
                    dstTrack = retargetRotationTrack(srcTrack, remap);
                } else if (srcTrack instanceof NodeEulerRotationTrack) {
                    dstTrack = retargetEulerToRotationTrack(srcTrack, remap);
                } else if (srcTrack instanceof NodeTranslationTrack) {
                    dstTrack = retargetTranslationTrack(srcTrack, remap);
                } else if (srcTrack instanceof NodeScaleTrack) {
                    dstTrack = new NodeScaleTrack(cloneInterpolator(srcTrack.interpolator));
                } else {
                    console.warn(`copyAnimationFrom: unsupported track type '${srcTrack.constructor.name}', skipping`);
                    continue;
                }
                dstTrack.name = srcTrack.name;
                dstTrack.target = dstNode.persistentId;
                dstTrack.jointIndex = remap.dstJointIndex;
                dstClip.addTrack(dstNode, dstTrack);
            }
        }
        return dstClip;
        function findRootJoint(joints) {
            let root = null;
            for (const joint of joints){
                if (!root) {
                    root = joint;
                }
                while(!root.isParentOf(joint)){
                    root = root.parent;
                }
                if (!root) {
                    break;
                }
            }
            if (!root || !joints.includes(root)) {
                return null;
            }
            return root;
        }
        function sortJoints(root, joints) {
            const ordered = [];
            const visited = new Set();
            function visit(joint) {
                if (visited.has(joint)) {
                    return true;
                }
                if (!joints.includes(joint)) {
                    return false;
                }
                if (joint !== root) {
                    visit(joint.parent);
                }
                visited.add(joint);
                ordered.push(joint);
                return true;
            }
            for (const joint of joints){
                if (!visit(joint)) {
                    return null;
                }
            }
            return ordered;
        }
    }
    /**
   * Dispose the animation set and release owned resources.
   *
   * - Disposes the weak reference to the model.
   * - Disposes all registered animation clips.
   * - Clears active animations, tracks, and skeleton references.
   */ onDispose() {
        super.onDispose();
        for(const k in this._animations){
            this._animations[k].dispose();
        }
        this._animations = {};
        this._activeAnimations.clear();
        this._activeSkeletons.clear();
        this._activeTracks.clear();
    }
}

export { AnimationSet };
//# sourceMappingURL=animationset.js.map
