import { SteeringBehavior } from '../SteeringBehavior.js';
import { Path } from '../Path.js';
import { Vector3 } from '../../math/Vector3.js';
import { LineSegment } from '../../math/LineSegment.js';
import { SeekBehavior } from './SeekBehavior.js';

const translation = new Vector3();
const predictedPosition = new Vector3();
const normalPoint = new Vector3();
const lineSegment = new LineSegment();
const closestNormalPoint = new Vector3();

/**
* This steering behavior produces a force that keeps a vehicle close to its path. It is intended
* to use it in combination with {@link FollowPathBehavior} in order to realize a more strict path following.
*
* @author {@link https://github.com/Mugen87|Mugen87}
* @augments SteeringBehavior
*/
class OnPathBehavior extends SteeringBehavior {

	/**
	* Constructs a new on path behavior.
	*
	* @param {Path} path - The path to stay close to.
	* @param {Number} radius - Defines the width of the path. With a smaller radius, the vehicle will have to follow the path more closely.
	* @param {Number} predictionFactor - Determines how far the behavior predicts the movement of the vehicle.
	*/
	constructor( path = new Path(), radius = 0.1, predictionFactor = 1 ) {

		super();

		/**
		* The path to stay close to.
		* @type {Path}
		*/
		this.path = path;

		/**
		* Defines the width of the path. With a smaller radius, the vehicle will have to follow the path more closely.
		* @type {Number}
		* @default 0.1
		*/
		this.radius = radius;

		/**
		* Determines how far the behavior predicts the movement of the vehicle.
		* @type {Number}
		* @default 1
		*/
		this.predictionFactor = predictionFactor;

		// internal behaviors

		this._seek = new SeekBehavior();

	}

	/**
	* Calculates the steering force for a single simulation step.
	*
	* @param {Vehicle} vehicle - The game entity the force is produced for.
	* @param {Vector3} force - The force/result vector.
	* @param {Number} delta - The time delta.
	* @return {Vector3} The force/result vector.
	*/
	calculate( vehicle, force /*, delta */ ) {

		const path = this.path;

		// predicted future position

		translation.copy( vehicle.velocity ).multiplyScalar( this.predictionFactor );
		predictedPosition.addVectors( vehicle.position, translation );

		// compute closest line segment and normal point. the normal point is computed by projecting
		// the predicted position of the vehicle on a line segment.

		let minDistance = Infinity;

		let l = path._waypoints.length;

		// handle looped paths differently since they have one line segment more

		l = ( path.loop === true ) ? l : l - 1;

		for ( let i = 0; i < l; i ++ ) {

			lineSegment.from = path._waypoints[ i ];

			// the last waypoint needs to be handled differently for a looped path.
			// connect the last point with the first one in order to create the last line segment

			if ( path.loop === true && i === ( l - 1 ) ) {

				lineSegment.to = path._waypoints[ 0 ];

			} else {

				lineSegment.to = path._waypoints[ i + 1 ];

			}

			lineSegment.closestPointToPoint( predictedPosition, true, normalPoint );

			const distance = predictedPosition.squaredDistanceTo( normalPoint );

			if ( distance < minDistance ) {

				minDistance = distance;
				closestNormalPoint.copy( normalPoint );

			}

		}

		// seek towards the projected point on the closest line segment if
		// the predicted position of the vehicle is outside the valid range.
		// also ensure that the path length is greater than zero when performing a seek

		if ( minDistance > ( this.radius * this.radius ) && path._waypoints.length > 1 ) {

			this._seek.target = closestNormalPoint;
			this._seek.calculate( vehicle, force );

		}

		return force;

	}

	/**
	* Transforms this instance into a JSON object.
	*
	* @return {Object} The JSON object.
	*/
	toJSON() {

		const json = super.toJSON();

		json.path = this.path.toJSON();
		json.radius = this.radius;
		json.predictionFactor = this.predictionFactor;

		return json;

	}

	/**
	* Restores this instance from the given JSON object.
	*
	* @param {Object} json - The JSON object.
	* @return {OnPathBehavior} A reference to this behavior.
	*/
	fromJSON( json ) {

		super.fromJSON( json );

		this.path.fromJSON( json.path );
		this.radius = json.radius;
		this.predictionFactor = json.predictionFactor;

		return this;

	}

}

export { OnPathBehavior };