import { Ray } from '../../math/Ray.js';
import { Vector3 } from '../../math/Vector3.js';

const toPoint = new Vector3();
const direction = new Vector3();
const ray = new Ray();
const intersectionPoint = new Vector3();
const worldPosition = new Vector3();

/**
 * Class for representing the vision component of a game entity.
 *
 * @author {@link https://github.com/Mugen87|Mugen87}
 */
class Vision {

	/**
	 * Constructs a new vision object.
	 *
	 * @param {GameEntity} owner - The owner of this vision instance.
	 */
	constructor( owner = null ) {

		/**
		 * The game entity that owns this vision instance.
		 * @type {?GameEntity}
		* @default null
		 */
		this.owner = owner;

		/**
		 * The field of view in radians.
		 * @type {Number}
		 * @default π
		 */
		this.fieldOfView = Math.PI;

		/**
		 * The visual range in world units.
		 * @type {Number}
		 * @default Infinity
		 */
		this.range = Infinity;

		/**
		 * An array of obstacles. An obstacle is a game entity that
		 * implements the {@link GameEntity#lineOfSightTest} method.
		 * @type {Array<GameEntity>}
		 */
		this.obstacles = new Array();

	}

	/**
	 * Adds an obstacle to this vision instance.
	 *
	 * @param {GameEntity} obstacle - The obstacle to add.
	 * @return {Vision} A reference to this vision instance.
	 */
	addObstacle( obstacle ) {

		this.obstacles.push( obstacle );

		return this;

	}

	/**
	 * Removes an obstacle from this vision instance.
	 *
	 * @param {GameEntity} obstacle - The obstacle to remove.
	 * @return {Vision} A reference to this vision instance.
	 */
	removeObstacle( obstacle ) {

		const index = this.obstacles.indexOf( obstacle );
		this.obstacles.splice( index, 1 );

		return this;

	}

	/**
	 * Performs a line of sight test in order to determine if the given point
	 * in 3D space is visible for the game entity.
	 *
	 * @param {Vector3} point - The point to test.
	 * @return {Boolean} Whether the given point is visible or not.
	 */
	visible( point ) {

		const owner = this.owner;
		const obstacles = this.obstacles;

		owner.getWorldPosition( worldPosition );

		// check if point lies within the game entity's visual range

		toPoint.subVectors( point, worldPosition );
		const distanceToPoint = toPoint.length();

		if ( distanceToPoint > this.range ) return false;

		// next, check if the point lies within the game entity's field of view

		owner.getWorldDirection( direction );

		const angle = direction.angleTo( toPoint );

		if ( angle > ( this.fieldOfView * 0.5 ) ) return false;

		// the point lies within the game entity's visual range and field
		// of view. now check if obstacles block the game entity's view to the given point.

		ray.origin.copy( worldPosition );
		ray.direction.copy( toPoint ).divideScalar( distanceToPoint || 1 ); // normalize

		for ( let i = 0, l = obstacles.length; i < l; i ++ ) {

			const obstacle = obstacles[ i ];

			const intersection = obstacle.lineOfSightTest( ray, intersectionPoint );

			if ( intersection !== null ) {

				// if an intersection point is closer to the game entity than the given point,
				// something is blocking the game entity's view

				const squaredDistanceToIntersectionPoint = intersectionPoint.squaredDistanceTo( worldPosition );

				if ( squaredDistanceToIntersectionPoint <= ( distanceToPoint * distanceToPoint ) ) return false;

			}

		}

		return true;

	}

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

		const json = {
			type: this.constructor.name,
			owner: this.owner.uuid,
			fieldOfView: this.fieldOfView,
			range: this.range.toString()
		};

		json.obstacles = new Array();

		for ( let i = 0, l = this.obstacles.length; i < l; i ++ ) {

			const obstacle = this.obstacles[ i ];
			json.obstacles.push( obstacle.uuid );

		}

		return json;

	}

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

		this.owner = json.owner;
		this.fieldOfView = json.fieldOfView;
		this.range = parseFloat( json.range );

		for ( let i = 0, l = json.obstacles.length; i < l; i ++ ) {

			const obstacle = json.obstacles[ i ];
			this.obstacles.push( obstacle );

		}

		return this;

	}

	/**
	 * Restores UUIDs with references to GameEntity objects.
	 *
	 * @param {Map<String,GameEntity>} entities - Maps game entities to UUIDs.
	 * @return {Vision} A reference to this vision.
	 */
	resolveReferences( entities ) {

		this.owner = entities.get( this.owner ) || null;

		const obstacles = this.obstacles;

		for ( let i = 0, l = obstacles.length; i < l; i ++ ) {

			obstacles[ i ] = entities.get( obstacles[ i ] );

		}

		return this;

	}

}

export { Vision };