/**
 * @author alteredq / http://alteredqualia.com/
 */

THREE.Car = function () {

	var scope = this;

	// car geometry manual parameters

	this.modelScale = 1;

	this.backWheelOffset = 2;

	this.autoWheelGeometry = true;

	// car geometry parameters automatically set from wheel mesh
	// 	- assumes wheel mesh is front left wheel in proper global
	//    position with respect to body mesh
	//	- other wheels are mirrored against car root
	//	- if necessary back wheels can be offset manually

	this.wheelOffset = new THREE.Vector3();

	this.wheelDiameter = 1;

	// car "feel" parameters

	this.MAX_SPEED = 2200;
	this.MAX_REVERSE_SPEED = -1500;

	this.MAX_WHEEL_ROTATION = 0.6;

	this.FRONT_ACCELERATION = 1250;
	this.BACK_ACCELERATION = 1500;

	this.WHEEL_ANGULAR_ACCELERATION = 1.5;

	this.FRONT_DECCELERATION = 750;
	this.WHEEL_ANGULAR_DECCELERATION = 1.0;

	this.STEERING_RADIUS_RATIO = 0.0023;

	this.MAX_TILT_SIDES = 0.05;
	this.MAX_TILT_FRONTBACK = 0.015;

	// internal control variables

	this.speed = 0;
	this.acceleration = 0;

	this.wheelOrientation = 0;
	this.carOrientation = 0;

	// car rigging

	this.root = new THREE.Object3D();

	this.frontLeftWheelRoot = new THREE.Object3D();
	this.frontRightWheelRoot = new THREE.Object3D();

	this.bodyMesh = null;

	this.frontLeftWheelMesh = null;
	this.frontRightWheelMesh = null;

	this.backLeftWheelMesh = null;
	this.backRightWheelMesh = null;

	this.bodyGeometry = null;
	this.wheelGeometry = null;

	this.bodyMaterials = null;
	this.wheelMaterials = null;

	// internal helper variables

	this.loaded = false;

	this.meshes = [];

	// API

	this.enableShadows = function ( enable ) {

		for ( var i = 0; i < this.meshes.length; i ++ ) {

			this.meshes[ i ].castShadow = enable;
			this.meshes[ i ].receiveShadow = enable;

		}

	};

	this.setVisible = function ( enable ) {

		for ( var i = 0; i < this.meshes.length; i ++ ) {

			this.meshes[ i ].visible = enable;
			this.meshes[ i ].visible = enable;

		}

	};

	this.loadPartsJSON = function ( bodyURL, wheelURL ) {

		var loader = new THREE.JSONLoader();

		loader.load( bodyURL, function( geometry, materials ) { createBody( geometry, materials ) } );
		loader.load( wheelURL, function( geometry, materials ) { createWheels( geometry, materials ) } );

	};

	this.loadPartsBinary = function ( bodyURL, wheelURL ) {

		var loader = new THREE.BinaryLoader();

		loader.load( bodyURL, function( geometry, materials ) { createBody( geometry, materials ) } );
		loader.load( wheelURL, function( geometry, materials ) { createWheels( geometry, materials ) } );

	};

	this.updateCarModel = function ( delta, controls ) {

		// speed and wheels based on controls

		if ( controls.moveForward ) {

			this.speed = THREE.Math.clamp( this.speed + delta * this.FRONT_ACCELERATION, this.MAX_REVERSE_SPEED, this.MAX_SPEED );
			this.acceleration = THREE.Math.clamp( this.acceleration + delta, -1, 1 );

		}

		if ( controls.moveBackward ) {


			this.speed = THREE.Math.clamp( this.speed - delta * this.BACK_ACCELERATION, this.MAX_REVERSE_SPEED, this.MAX_SPEED );
			this.acceleration = THREE.Math.clamp( this.acceleration - delta, -1, 1 );

		}

		if ( controls.moveLeft ) {

			this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation + delta * this.WHEEL_ANGULAR_ACCELERATION, - this.MAX_WHEEL_ROTATION, this.MAX_WHEEL_ROTATION );

		}

		if ( controls.moveRight ) {

			this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation - delta * this.WHEEL_ANGULAR_ACCELERATION, - this.MAX_WHEEL_ROTATION, this.MAX_WHEEL_ROTATION );

		}

		// speed decay

		if ( ! ( controls.moveForward || controls.moveBackward ) ) {

			if ( this.speed > 0 ) {

				var k = exponentialEaseOut( this.speed / this.MAX_SPEED );

				this.speed = THREE.Math.clamp( this.speed - k * delta * this.FRONT_DECCELERATION, 0, this.MAX_SPEED );
				this.acceleration = THREE.Math.clamp( this.acceleration - k * delta, 0, 1 );

			} else {

				var k = exponentialEaseOut( this.speed / this.MAX_REVERSE_SPEED );

				this.speed = THREE.Math.clamp( this.speed + k * delta * this.BACK_ACCELERATION, this.MAX_REVERSE_SPEED, 0 );
				this.acceleration = THREE.Math.clamp( this.acceleration + k * delta, -1, 0 );

			}


		}

		// steering decay

		if ( ! ( controls.moveLeft || controls.moveRight ) ) {

			if ( this.wheelOrientation > 0 ) {

				this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation - delta * this.WHEEL_ANGULAR_DECCELERATION, 0, this.MAX_WHEEL_ROTATION );

			} else {

				this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation + delta * this.WHEEL_ANGULAR_DECCELERATION, - this.MAX_WHEEL_ROTATION, 0 );

			}

		}

		// car update

		var forwardDelta = this.speed * delta;

		this.carOrientation += ( forwardDelta * this.STEERING_RADIUS_RATIO )* this.wheelOrientation;

		// displacement

		this.root.position.x += Math.sin( this.carOrientation ) * forwardDelta;
		this.root.position.z += Math.cos( this.carOrientation ) * forwardDelta;

		// steering

		this.root.rotation.y = this.carOrientation;

		// tilt

		if ( this.loaded ) {

			this.bodyMesh.rotation.z = this.MAX_TILT_SIDES * this.wheelOrientation * ( this.speed / this.MAX_SPEED );
			this.bodyMesh.rotation.x = - this.MAX_TILT_FRONTBACK * this.acceleration;

		}

		// wheels rolling

		var angularSpeedRatio = 1 / ( this.modelScale * ( this.wheelDiameter / 2 ) );

		var wheelDelta = forwardDelta * angularSpeedRatio;

		if ( this.loaded ) {

			this.frontLeftWheelMesh.rotation.x += wheelDelta;
			this.frontRightWheelMesh.rotation.x += wheelDelta;
			this.backLeftWheelMesh.rotation.x += wheelDelta;
			this.backRightWheelMesh.rotation.x += wheelDelta;

		}

		// front wheels steering

		this.frontLeftWheelRoot.rotation.y = this.wheelOrientation;
		this.frontRightWheelRoot.rotation.y = this.wheelOrientation;

	};

	// internal helper methods

	function createBody ( geometry, materials ) {

		scope.bodyGeometry = geometry;
		scope.bodyMaterials = materials;

		createCar();

	};

	function createWheels ( geometry, materials ) {

		scope.wheelGeometry = geometry;
		scope.wheelMaterials = materials;

		createCar();

	};

	function createCar () {

		if ( scope.bodyGeometry && scope.wheelGeometry ) {

			// compute wheel geometry parameters

			if ( scope.autoWheelGeometry ) {

				scope.wheelGeometry.computeBoundingBox();

				var bb = scope.wheelGeometry.boundingBox;

				scope.wheelOffset.add( bb.min, bb.max );
				scope.wheelOffset.multiplyScalar( 0.5 );

				scope.wheelDiameter = bb.max.y - bb.min.y;

				THREE.GeometryUtils.center( scope.wheelGeometry );

			}

			// rig the car

			var s = scope.modelScale,
				delta = new THREE.Vector3();

			var bodyFaceMaterial = new THREE.MeshFaceMaterial( scope.bodyMaterials );
			var wheelFaceMaterial = new THREE.MeshFaceMaterial( scope.wheelMaterials );

			// body

			scope.bodyMesh = new THREE.Mesh( scope.bodyGeometry, bodyFaceMaterial );
			scope.bodyMesh.scale.set( s, s, s );

			scope.root.add( scope.bodyMesh );

			// front left wheel

			delta.multiply( scope.wheelOffset, new THREE.Vector3( s, s, s ) );

			scope.frontLeftWheelRoot.position.addSelf( delta );

			scope.frontLeftWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );
			scope.frontLeftWheelMesh.scale.set( s, s, s );

			scope.frontLeftWheelRoot.add( scope.frontLeftWheelMesh );
			scope.root.add( scope.frontLeftWheelRoot );

			// front right wheel

			delta.multiply( scope.wheelOffset, new THREE.Vector3( -s, s, s ) );

			scope.frontRightWheelRoot.position.addSelf( delta );

			scope.frontRightWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );

			scope.frontRightWheelMesh.scale.set( s, s, s );
			scope.frontRightWheelMesh.rotation.z = Math.PI;

			scope.frontRightWheelRoot.add( scope.frontRightWheelMesh );
			scope.root.add( scope.frontRightWheelRoot );

			// back left wheel

			delta.multiply( scope.wheelOffset, new THREE.Vector3( s, s, -s ) );
			delta.z -= scope.backWheelOffset;

			scope.backLeftWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );

			scope.backLeftWheelMesh.position.addSelf( delta );
			scope.backLeftWheelMesh.scale.set( s, s, s );

			scope.root.add( scope.backLeftWheelMesh );

			// back right wheel

			delta.multiply( scope.wheelOffset, new THREE.Vector3( -s, s, -s ) );
			delta.z -= scope.backWheelOffset;

			scope.backRightWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );

			scope.backRightWheelMesh.position.addSelf( delta );
			scope.backRightWheelMesh.scale.set( s, s, s );
			scope.backRightWheelMesh.rotation.z = Math.PI;

			scope.root.add( scope.backRightWheelMesh );

			// cache meshes

			scope.meshes = [ scope.bodyMesh, scope.frontLeftWheelMesh, scope.frontRightWheelMesh, scope.backLeftWheelMesh, scope.backRightWheelMesh ];

			// callback

			scope.loaded = true;

			if ( scope.callback ) {

				scope.callback( scope );

			}

		}

	};

	function quadraticEaseOut( k ) { return - k * ( k - 2 ); }
	function cubicEaseOut( k ) { return --k * k * k + 1; }
	function circularEaseOut( k ) { return Math.sqrt( 1 - --k * k ); }
	function sinusoidalEaseOut( k ) { return Math.sin( k * Math.PI / 2 ); }
	function exponentialEaseOut( k ) { return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1; }

};
