(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(["exports", "three"], factory);
  } else if (typeof exports !== "undefined") {
    factory(exports, require("three"));
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports, global.three);
    global.SkeletonUtils = mod.exports;
  }
})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports, _three) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.clone = clone;
  _exports.findBoneTrackData = findBoneTrackData;
  _exports.getBoneByName = getBoneByName;
  _exports.getBones = getBones;
  _exports.getEqualsBonesNames = getEqualsBonesNames;
  _exports.getHelperFromSkeleton = getHelperFromSkeleton;
  _exports.getNearestBone = getNearestBone;
  _exports.getSkeletonOffsets = getSkeletonOffsets;
  _exports.renameBones = renameBones;
  _exports.retarget = retarget;
  _exports.retargetClip = retargetClip;

  function retarget(target, source) {
    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
    var pos = new _three.Vector3(),
        quat = new _three.Quaternion(),
        scale = new _three.Vector3(),
        bindBoneMatrix = new _three.Matrix4(),
        relativeMatrix = new _three.Matrix4(),
        globalMatrix = new _three.Matrix4();
    options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
    options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
    options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
    options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
    options.hip = options.hip !== undefined ? options.hip : 'hip';
    options.names = options.names || {};
    var sourceBones = source.isObject3D ? source.skeleton.bones : getBones(source),
        bones = target.isObject3D ? target.skeleton.bones : getBones(target);
    var bindBones, bone, name, boneTo, bonesPosition; // reset bones

    if (target.isObject3D) {
      target.skeleton.pose();
    } else {
      options.useTargetMatrix = true;
      options.preserveMatrix = false;
    }

    if (options.preservePosition) {
      bonesPosition = [];

      for (var i = 0; i < bones.length; i++) {
        bonesPosition.push(bones[i].position.clone());
      }
    }

    if (options.preserveMatrix) {
      // reset matrix
      target.updateMatrixWorld();
      target.matrixWorld.identity(); // reset children matrix

      for (var _i = 0; _i < target.children.length; ++_i) {
        target.children[_i].updateMatrixWorld(true);
      }
    }

    if (options.offsets) {
      bindBones = [];

      for (var _i2 = 0; _i2 < bones.length; ++_i2) {
        bone = bones[_i2];
        name = options.names[bone.name] || bone.name;

        if (options.offsets && options.offsets[name]) {
          bone.matrix.multiply(options.offsets[name]);
          bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);
          bone.updateMatrixWorld();
        }

        bindBones.push(bone.matrixWorld.clone());
      }
    }

    for (var _i3 = 0; _i3 < bones.length; ++_i3) {
      bone = bones[_i3];
      name = options.names[bone.name] || bone.name;
      boneTo = getBoneByName(name, sourceBones);
      globalMatrix.copy(bone.matrixWorld);

      if (boneTo) {
        boneTo.updateMatrixWorld();

        if (options.useTargetMatrix) {
          relativeMatrix.copy(boneTo.matrixWorld);
        } else {
          relativeMatrix.copy(target.matrixWorld).invert();
          relativeMatrix.multiply(boneTo.matrixWorld);
        } // ignore scale to extract rotation


        scale.setFromMatrixScale(relativeMatrix);
        relativeMatrix.scale(scale.set(1 / scale.x, 1 / scale.y, 1 / scale.z)); // apply to global matrix

        globalMatrix.makeRotationFromQuaternion(quat.setFromRotationMatrix(relativeMatrix));

        if (target.isObject3D) {
          var boneIndex = bones.indexOf(bone),
              wBindMatrix = bindBones ? bindBones[boneIndex] : bindBoneMatrix.copy(target.skeleton.boneInverses[boneIndex]).invert();
          globalMatrix.multiply(wBindMatrix);
        }

        globalMatrix.copyPosition(relativeMatrix);
      }

      if (bone.parent && bone.parent.isBone) {
        bone.matrix.copy(bone.parent.matrixWorld).invert();
        bone.matrix.multiply(globalMatrix);
      } else {
        bone.matrix.copy(globalMatrix);
      }

      if (options.preserveHipPosition && name === options.hip) {
        bone.matrix.setPosition(pos.set(0, bone.position.y, 0));
      }

      bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);
      bone.updateMatrixWorld();
    }

    if (options.preservePosition) {
      for (var _i4 = 0; _i4 < bones.length; ++_i4) {
        bone = bones[_i4];
        name = options.names[bone.name] || bone.name;

        if (name !== options.hip) {
          bone.position.copy(bonesPosition[_i4]);
        }
      }
    }

    if (options.preserveMatrix) {
      // restore matrix
      target.updateMatrixWorld(true);
    }
  }

  function retargetClip(target, source, clip) {
    var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
    options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
    options.fps = options.fps !== undefined ? options.fps : 30;
    options.names = options.names || [];

    if (!source.isObject3D) {
      source = getHelperFromSkeleton(source);
    }

    var numFrames = Math.round(clip.duration * (options.fps / 1000) * 1000),
        delta = 1 / options.fps,
        convertedTracks = [],
        mixer = new _three.AnimationMixer(source),
        bones = getBones(target.skeleton),
        boneDatas = [];
    var positionOffset, bone, boneTo, boneData, name;
    mixer.clipAction(clip).play();
    mixer.update(0);
    source.updateMatrixWorld();

    for (var i = 0; i < numFrames; ++i) {
      var time = i * delta;
      retarget(target, source, options);

      for (var j = 0; j < bones.length; ++j) {
        name = options.names[bones[j].name] || bones[j].name;
        boneTo = getBoneByName(name, source.skeleton);

        if (boneTo) {
          bone = bones[j];
          boneData = boneDatas[j] = boneDatas[j] || {
            bone: bone
          };

          if (options.hip === name) {
            if (!boneData.pos) {
              boneData.pos = {
                times: new Float32Array(numFrames),
                values: new Float32Array(numFrames * 3)
              };
            }

            if (options.useFirstFramePosition) {
              if (i === 0) {
                positionOffset = bone.position.clone();
              }

              bone.position.sub(positionOffset);
            }

            boneData.pos.times[i] = time;
            bone.position.toArray(boneData.pos.values, i * 3);
          }

          if (!boneData.quat) {
            boneData.quat = {
              times: new Float32Array(numFrames),
              values: new Float32Array(numFrames * 4)
            };
          }

          boneData.quat.times[i] = time;
          bone.quaternion.toArray(boneData.quat.values, i * 4);
        }
      }

      mixer.update(delta);
      source.updateMatrixWorld();
    }

    for (var _i5 = 0; _i5 < boneDatas.length; ++_i5) {
      boneData = boneDatas[_i5];

      if (boneData) {
        if (boneData.pos) {
          convertedTracks.push(new _three.VectorKeyframeTrack('.bones[' + boneData.bone.name + '].position', boneData.pos.times, boneData.pos.values));
        }

        convertedTracks.push(new _three.QuaternionKeyframeTrack('.bones[' + boneData.bone.name + '].quaternion', boneData.quat.times, boneData.quat.values));
      }
    }

    mixer.uncacheAction(clip);
    return new _three.AnimationClip(clip.name, -1, convertedTracks);
  }

  function getHelperFromSkeleton(skeleton) {
    var source = new _three.SkeletonHelper(skeleton.bones[0]);
    source.skeleton = skeleton;
    return source;
  }

  function getSkeletonOffsets(target, source) {
    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
    var targetParentPos = new _three.Vector3(),
        targetPos = new _three.Vector3(),
        sourceParentPos = new _three.Vector3(),
        sourcePos = new _three.Vector3(),
        targetDir = new _three.Vector2(),
        sourceDir = new _three.Vector2();
    options.hip = options.hip !== undefined ? options.hip : 'hip';
    options.names = options.names || {};

    if (!source.isObject3D) {
      source = getHelperFromSkeleton(source);
    }

    var nameKeys = Object.keys(options.names),
        nameValues = Object.values(options.names),
        sourceBones = source.isObject3D ? source.skeleton.bones : getBones(source),
        bones = target.isObject3D ? target.skeleton.bones : getBones(target),
        offsets = [];
    var bone, boneTo, name, i;
    target.skeleton.pose();

    for (i = 0; i < bones.length; ++i) {
      bone = bones[i];
      name = options.names[bone.name] || bone.name;
      boneTo = getBoneByName(name, sourceBones);

      if (boneTo && name !== options.hip) {
        var boneParent = getNearestBone(bone.parent, nameKeys),
            boneToParent = getNearestBone(boneTo.parent, nameValues);
        boneParent.updateMatrixWorld();
        boneToParent.updateMatrixWorld();
        targetParentPos.setFromMatrixPosition(boneParent.matrixWorld);
        targetPos.setFromMatrixPosition(bone.matrixWorld);
        sourceParentPos.setFromMatrixPosition(boneToParent.matrixWorld);
        sourcePos.setFromMatrixPosition(boneTo.matrixWorld);
        targetDir.subVectors(new _three.Vector2(targetPos.x, targetPos.y), new _three.Vector2(targetParentPos.x, targetParentPos.y)).normalize();
        sourceDir.subVectors(new _three.Vector2(sourcePos.x, sourcePos.y), new _three.Vector2(sourceParentPos.x, sourceParentPos.y)).normalize();
        var laterialAngle = targetDir.angle() - sourceDir.angle();
        var offset = new _three.Matrix4().makeRotationFromEuler(new _three.Euler(0, 0, laterialAngle));
        bone.matrix.multiply(offset);
        bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);
        bone.updateMatrixWorld();
        offsets[name] = offset;
      }
    }

    return offsets;
  }

  function renameBones(skeleton, names) {
    var bones = getBones(skeleton);

    for (var i = 0; i < bones.length; ++i) {
      var bone = bones[i];

      if (names[bone.name]) {
        bone.name = names[bone.name];
      }
    }

    return this;
  }

  function getBones(skeleton) {
    return Array.isArray(skeleton) ? skeleton : skeleton.bones;
  }

  function getBoneByName(name, skeleton) {
    for (var i = 0, bones = getBones(skeleton); i < bones.length; i++) {
      if (name === bones[i].name) return bones[i];
    }
  }

  function getNearestBone(bone, names) {
    while (bone.isBone) {
      if (names.indexOf(bone.name) !== -1) {
        return bone;
      }

      bone = bone.parent;
    }
  }

  function findBoneTrackData(name, tracks) {
    var regexp = /\[(.*)\]\.(.*)/,
        result = {
      name: name
    };

    for (var i = 0; i < tracks.length; ++i) {
      // 1 is track name
      // 2 is track type
      var trackData = regexp.exec(tracks[i].name);

      if (trackData && name === trackData[1]) {
        result[trackData[2]] = i;
      }
    }

    return result;
  }

  function getEqualsBonesNames(skeleton, targetSkeleton) {
    var sourceBones = getBones(skeleton),
        targetBones = getBones(targetSkeleton),
        bones = [];

    search: for (var i = 0; i < sourceBones.length; i++) {
      var boneName = sourceBones[i].name;

      for (var j = 0; j < targetBones.length; j++) {
        if (boneName === targetBones[j].name) {
          bones.push(boneName);
          continue search;
        }
      }
    }

    return bones;
  }

  function clone(source) {
    var sourceLookup = new Map();
    var cloneLookup = new Map();
    var clone = source.clone();
    parallelTraverse(source, clone, function (sourceNode, clonedNode) {
      sourceLookup.set(clonedNode, sourceNode);
      cloneLookup.set(sourceNode, clonedNode);
    });
    clone.traverse(function (node) {
      if (!node.isSkinnedMesh) return;
      var clonedMesh = node;
      var sourceMesh = sourceLookup.get(node);
      var sourceBones = sourceMesh.skeleton.bones;
      clonedMesh.skeleton = sourceMesh.skeleton.clone();
      clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix);
      clonedMesh.skeleton.bones = sourceBones.map(function (bone) {
        return cloneLookup.get(bone);
      });
      clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix);
    });
    return clone;
  }

  function parallelTraverse(a, b, callback) {
    callback(a, b);

    for (var i = 0; i < a.children.length; i++) {
      parallelTraverse(a.children[i], b.children[i], callback);
    }
  }
});