import {
	AnimationClip,
	AnimationMixer,
	Euler,
	Matrix4,
	Quaternion,
	QuaternionKeyframeTrack,
	SkeletonHelper,
	Vector2,
	Vector3,
	VectorKeyframeTrack
} from '../../../build/three.module.js';

var SkeletonUtils = {

	retarget: function () {

		var pos = new Vector3(),
			quat = new Quaternion(),
			scale = new Vector3(),
			bindBoneMatrix = new Matrix4(),
			relativeMatrix = new Matrix4(),
			globalMatrix = new Matrix4();

		return function ( target, source, options ) {

			options = options || {};
			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 : this.getBones( source ),
				bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
				bindBones,
				bone, name, boneTo,
				bonesPosition, i;

			// reset bones

			if ( target.isObject3D ) {

				target.skeleton.pose();

			} else {

				options.useTargetMatrix = true;
				options.preserveMatrix = false;

			}

			if ( options.preservePosition ) {

				bonesPosition = [];

				for ( 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 ( i = 0; i < target.children.length; ++ i ) {

					target.children[ i ].updateMatrixWorld( true );

				}

			}

			if ( options.offsets ) {

				bindBones = [];

				for ( i = 0; i < bones.length; ++ i ) {

					bone = bones[ i ];
					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 ( i = 0; i < bones.length; ++ i ) {

				bone = bones[ i ];
				name = options.names[ bone.name ] || bone.name;

				boneTo = this.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 ( i = 0; i < bones.length; ++ i ) {

					bone = bones[ i ];
					name = options.names[ bone.name ] || bone.name;

					if ( name !== options.hip ) {

						bone.position.copy( bonesPosition[ i ] );

					}

				}

			}

			if ( options.preserveMatrix ) {

				// restore matrix

				target.updateMatrixWorld( true );

			}

		};

	}(),

	retargetClip: function ( target, source, clip, options ) {

		options = options || {};
		options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
		options.fps = options.fps !== undefined ? options.fps : 30;
		options.names = options.names || [];

		if ( ! source.isObject3D ) {

			source = this.getHelperFromSkeleton( source );

		}

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

		mixer.clipAction( clip ).play();
		mixer.update( 0 );

		source.updateMatrixWorld();

		for ( i = 0; i < numFrames; ++ i ) {

			var time = i * delta;

			this.retarget( target, source, options );

			for ( j = 0; j < bones.length; ++ j ) {

				name = options.names[ bones[ j ].name ] || bones[ j ].name;

				boneTo = this.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 ( i = 0; i < boneDatas.length; ++ i ) {

			boneData = boneDatas[ i ];

			if ( boneData ) {

				if ( boneData.pos ) {

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

				}

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

			}

		}

		mixer.uncacheAction( clip );

		return new AnimationClip( clip.name, - 1, convertedTracks );

	},

	getHelperFromSkeleton: function ( skeleton ) {

		var source = new SkeletonHelper( skeleton.bones[ 0 ] );
		source.skeleton = skeleton;

		return source;

	},

	getSkeletonOffsets: function () {

		var targetParentPos = new Vector3(),
			targetPos = new Vector3(),
			sourceParentPos = new Vector3(),
			sourcePos = new Vector3(),
			targetDir = new Vector2(),
			sourceDir = new Vector2();

		return function ( target, source, options ) {

			options = options || {};
			options.hip = options.hip !== undefined ? options.hip : 'hip';
			options.names = options.names || {};

			if ( ! source.isObject3D ) {

				source = this.getHelperFromSkeleton( source );

			}

			var nameKeys = Object.keys( options.names ),
				nameValues = Object.values( options.names ),
				sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ),
				bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
				offsets = [],
				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 = this.getBoneByName( name, sourceBones );

				if ( boneTo && name !== options.hip ) {

					var boneParent = this.getNearestBone( bone.parent, nameKeys ),
						boneToParent = this.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 Vector2( targetPos.x, targetPos.y ),
						new Vector2( targetParentPos.x, targetParentPos.y )
					).normalize();

					sourceDir.subVectors(
						new Vector2( sourcePos.x, sourcePos.y ),
						new Vector2( sourceParentPos.x, sourceParentPos.y )
					).normalize();

					var laterialAngle = targetDir.angle() - sourceDir.angle();

					var offset = new Matrix4().makeRotationFromEuler(
						new Euler(
							0,
							0,
							laterialAngle
						)
					);

					bone.matrix.multiply( offset );

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

					bone.updateMatrixWorld();

					offsets[ name ] = offset;

				}

			}

			return offsets;

		};

	}(),

	renameBones: function ( skeleton, names ) {

		var bones = this.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;

	},

	getBones: function ( skeleton ) {

		return Array.isArray( skeleton ) ? skeleton : skeleton.bones;

	},

	getBoneByName: function ( name, skeleton ) {

		for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) {

			if ( name === bones[ i ].name )

				return bones[ i ];

		}

	},

	getNearestBone: function ( bone, names ) {

		while ( bone.isBone ) {

			if ( names.indexOf( bone.name ) !== - 1 ) {

				return bone;

			}

			bone = bone.parent;

		}

	},

	findBoneTrackData: function ( 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;

	},

	getEqualsBonesNames: function ( skeleton, targetSkeleton ) {

		var sourceBones = this.getBones( skeleton ),
			targetBones = this.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;

	},

	clone: function ( 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 );

	}

}

export { SkeletonUtils };
