/**
* @author herzig / http://github.com/herzig
*
* Description: reads BVH files and outputs a single THREE.Skeleton and an THREE.AnimationClip
*
* Currently only supports bvh files containing a single root.
*
*/

THREE.BVHLoader = function( manager ) {

	this.animateBonePositions = true;
	this.animateBoneRotations = true;

	this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

};

THREE.BVHLoader.prototype = {

	constructor: THREE.BVHLoader,

	load: function ( url, onLoad, onProgress, onError ) {

		var scope = this;

		var loader = new THREE.XHRLoader( scope.manager );
		loader.setResponseType( 'arraybuffer' );
		loader.load( url, function( buffer ) {

			onLoad( scope.parse( buffer ) );

		}, onProgress, onError );

	},

	parse: function ( buffer ) {

		/*
			reads a string array (lines) from a BVH file
			and outputs a skeleton structure including motion data

			returns thee root node:
			{ name: "", channels: [], children: [] }
		*/
		function readBvh( lines ) {

			// read model structure
			if ( nextLine( lines ) !== "HIERARCHY" ) {

				throw "HIERARCHY expected";

			}

			var list = []; // collects flat array of all bones
			var root = readNode( lines, nextLine( lines ), list );

			// read motion data
			if ( nextLine( lines ) != "MOTION" ) {

				throw "MOTION  expected";

			}

			// number of frames
			var tokens = nextLine( lines ).split( /[\s]+/ );
			var numFrames = parseInt( tokens[ 1 ] );
			if ( isNaN( numFrames ) ) {

				throw "Failed to read number of frames.";

			}

			// frame time
			tokens = nextLine( lines ).split( /[\s]+/ );
			var frameTime = parseFloat( tokens[ 2 ] );
			if ( isNaN( frameTime ) ) {

				throw "Failed to read frame time.";

			}

			// read frame data line by line
			for ( var i = 0; i < numFrames; ++ i ) {

				tokens = nextLine( lines ).split( /[\s]+/ );

				readFrameData( tokens, i * frameTime, root, list );

			}

			return list;

		}

		/*
			Recursively reads data from a single frame into the bone hierarchy.
			The passed bone hierarchy has to be structured in the same order as the BVH file.
			keyframe data is stored in bone.frames.

			- data: splitted string array (frame values), values are shift()ed so
			this should be empty after parsing the whole hierarchy.
			- frameTime: playback time for this keyframe.
			- bone: the bone to read frame data from.
		*/
		function readFrameData( data, frameTime, bone ) {

			// end sites have no motion data
			if ( bone.type === "ENDSITE" ) {

				return;

			}

			// add keyframe
			var keyframe = {
				time: frameTime,
				position: { x: 0, y: 0, z: 0 },
				rotation: new THREE.Quaternion(),
			};

			bone.frames.push( keyframe );

			var quat = new THREE.Quaternion();

			var vx = new THREE.Vector3( 1, 0, 0 );
			var vy = new THREE.Vector3( 0, 1, 0 );
			var vz = new THREE.Vector3( 0, 0, 1 );

			// parse values for each channel in node
			for ( var i = 0; i < bone.channels.length; ++ i ) {

				switch ( bone.channels[ i ] ) {

				case "Xposition":
					keyframe.position.x = parseFloat( data.shift().trim() );
					break;
				case "Yposition":
					keyframe.position.y = parseFloat( data.shift().trim() );
					break;
				case "Zposition":
					keyframe.position.z = parseFloat( data.shift().trim() );
					break;
				case "Xrotation":
					quat.setFromAxisAngle( vx, parseFloat( data.shift().trim() ) * Math.PI / 180 );
					keyframe.rotation.multiply( quat );
					break;
				case "Yrotation":
					quat.setFromAxisAngle( vy, parseFloat( data.shift().trim() ) * Math.PI / 180 );
					keyframe.rotation.multiply( quat );
					break;
				case "Zrotation":
					quat.setFromAxisAngle( vz, parseFloat( data.shift().trim() ) * Math.PI / 180 );
					keyframe.rotation.multiply( quat );
					break;
				default:
					throw "invalid channel type";

				}

			}

			// parse child nodes
			for ( var i = 0; i < bone.children.length; ++ i ) {

				readFrameData( data, frameTime, bone.children[ i ] );

			}

		}

		/*
		 Recursively parses the HIERACHY section of the BVH file

		 - lines: all lines of the file. lines are consumed as we go along.
		 - firstline: line containing the node type and name e.g. "JOINT hip"
		 - list: collects a flat list of nodes

		 returns: a BVH node including children
		*/
		function readNode( lines, firstline, list ) {

			var node = { name: "", type: "", frames: [] };
			list.push( node );

			// parse node type and name.
			var tokens = firstline.split( /[\s]+/ );

			if ( tokens[ 0 ].toUpperCase() === "END" && tokens[ 1 ].toUpperCase() === "SITE" ) {

				node.type = "ENDSITE";
				node.name = "ENDSITE"; // bvh end sites have no name

			} else {

				node.name = tokens[ 1 ];
				node.type = tokens[ 0 ].toUpperCase();

			}

			if ( nextLine( lines ) != "{" ) {

				throw "Expected opening { after type & name";

			}

			// parse OFFSET
			tokens = nextLine( lines ).split( /[\s]+/ );

			if ( tokens[ 0 ] !== "OFFSET" ) {

				throw "Expected OFFSET, but got: " + tokens[ 0 ];

			}

			if ( tokens.length != 4 ) {

				throw "OFFSET: Invalid number of values";

			}

			var offset = {
				x: parseFloat( tokens[ 1 ] ),
				y: parseFloat( tokens[ 2 ] ),
				z: parseFloat( tokens[ 3 ] )
			};

			if ( isNaN( offset.x ) || isNaN( offset.y ) || isNaN( offset.z ) ) {

				throw "OFFSET: Invalid values";

			}

			node.offset = offset;

			// parse CHANNELS definitions
			if ( node.type != "ENDSITE" ) {

				tokens = nextLine( lines ).split( /[\s]+/ );

				if ( tokens[ 0 ] != "CHANNELS" ) {

					throw "Expected CHANNELS definition";

				}

				var numChannels = parseInt( tokens[ 1 ] );
				node.channels = tokens.splice( 2, numChannels );
				node.children = [];

			}

			// read children
			while ( true ) {

				var line = nextLine( lines );

				if ( line === "}" ) {

					return node;

				} else {

					node.children.push( readNode( lines, line, list ) );

				}

			}

		}

		/*
			recursively converts the internal bvh node structure to a THREE.Bone hierarchy

			source: the bvh root node
			list: pass an empty array, collects a flat list of all converted THREE.Bones

			returns the root THREE.Bone
		*/
		function toTHREEBone( source, list ) {

			var bone = new THREE.Bone();
			list.push( bone );

			bone.position.add( source.offset );
			bone.name = source.name;

			if ( source.type != "ENDSITE" ) {

				for ( var i = 0; i < source.children.length; ++ i ) {

					bone.add( toTHREEBone( source.children[ i ], list ) );

				}

			}

			return bone;

		}

		/*
			builds a THREE.AnimationClip from the keyframe data saved in each bone.

			bone: bvh root node

			returns: a THREE.AnimationClip containing position and quaternion tracks
		*/
		function toTHREEAnimation( bones ) {

			var tracks = [];

			// create a position and quaternion animation track for each node
			for ( var i = 0; i < bones.length; ++ i ) {

				var bone = bones[ i ];

				if ( bone.type == "ENDSITE" )
					continue;

				// track data
				var times = [];
				var positions = [];
				var rotations = [];

				for ( var j = 0; j < bone.frames.length; ++ j ) {

					var frame = bone.frames[ j ];

					times.push( frame.time );

					// the animation system animates the position property,
					// so we have to add the joint offset to all values
					positions.push( frame.position.x + bone.offset.x );
					positions.push( frame.position.y + bone.offset.y );
					positions.push( frame.position.z + bone.offset.z );

					rotations.push( frame.rotation.x );
					rotations.push( frame.rotation.y );
					rotations.push( frame.rotation.z );
					rotations.push( frame.rotation.w );

				}

				if ( scope.animateBonePositions ) {

					tracks.push( new THREE.VectorKeyframeTrack(
						".bones[" + bone.name + "].position", times, positions ) );

				}

				if ( scope.animateBoneRotations ) {

					tracks.push( new THREE.QuaternionKeyframeTrack(
						".bones[" + bone.name + "].quaternion", times, rotations ) );

				}

			}

			return new THREE.AnimationClip( "animation", - 1, tracks );

		}

		/*
			returns the next non-empty line in lines
		*/
		function nextLine( lines ) {

			var line;
			// skip empty lines
			while ( ( line = lines.shift().trim() ).length === 0 ) { }
			return line;

		}

		var scope = this;

		// convert buffer to ASCII string
		var text = "";
		var raw = new Uint8Array( buffer );
		for ( var i = 0; i < raw.length; ++ i ) {

			text += String.fromCharCode( raw[ i ] );

		}

		var lines = text.split( /[\r\n]+/g );

		var bones = readBvh( lines );

		var threeBones = [];
		toTHREEBone( bones[ 0 ], threeBones );

		var threeClip = toTHREEAnimation( bones );

		return {
			skeleton: new THREE.Skeleton( threeBones ),
			clip: threeClip
		};

	}

};
