'use strict';
/**
 * Vector2 Class
 * @property {Number} x The x value of the vertice
 * @property {Number} y The y value of the vertice
 */
class Vector2 {
	/**
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * // returns Vector2 with x:0 & y:0
	 * new Vector2()
	 * @example
	 * // returns Vector2 with x:2 & y:2
	 * new Vector2(2)
	 * @example
	 * // returns Vector2 with x:4 & y:5
	 * new Vector2(4, 5)
	 * @example
	 * // returns Vector2 with x:1 & y:3
	 * new Vector2({x: 1, y: 3})
	 * @example
	 * // returns Vector2 with x:6 & y:7
	 * const v1 = new Vector2(6, 7);
	 * new Vector2(v1);
	 * @constructor
	 */
	constructor(...args) {
		this.x = 0;
		this.y = 0;
		const pos = this._checkParams(args);
		this.x = pos.x;
		this.y = pos.y;
		this.next = null;
		this.prev = null;
	}

	/**
	 * Flooring the Vector2
	 * @param {Number} v=1 Precision
	 * @returns {Vector2}
	 */
	floor(v = 1) {
		this.floorX(v);
		this.floorY(v);
		return this;
	}

	/**
	 * Flooring the x value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	floorX(v = 1) {
		this.x = Math.floor(this.x * v) / v;
		return this;
	}

	/**
	 * Flooring the y value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	floorY(v = 1) {
		this.y = Math.floor(this.y * v) / v;
		return this;
	}


	/**
	 * Ceiling the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	ceil(v = 1) {
		this.ceilX(v);
		this.ceilY(v);
		return this;
	}

	/**
	 * Ceiling the x value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	ceilX(v = 1) {
		this.x = Math.ceil(this.x * v) / v;
		return this;
	}

	/**
	 * Ceiling the y value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	ceilY(v = 1) {
		this.y = Math.ceil(this.y * v) / v;
		return this;
	}

	/**
	 * Rounding the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	round(v = 1) {
		this.roundX(v);
		this.roundY(v);
		return this;
	}

	/**
	 * Rounding the x value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	roundX(v = 1) {
		this.x = Math.round(this.x * v) / v;
		return this;
	}

	/**
	 * Rounding the y value of the Vector2
	 * @param {Number} [v=1] Precision
	 * @returns {Vector2}
	 */
	roundY(v = 1) {
		this.y = Math.round(this.y * v) / v;
		return this;
	}

	/**
	 * Setting the value of the Vector2
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * const v1 = new Vector2();
	 * // change v1 Vector2 to x:3 & y:3
	 * v1.set(3);
	 * @example
	 * const v1 = new Vector2();
	 * // change v1 Vector2 to x:3 & y:4
	 * v1.set(3, 4);
	 * @example
	 * const v1 = new Vector2();
	 * // change v1 Vector2 to x:5 & y:6
	 * v1.set({x:5, y:6});
	 * @example
	 * const v1 = new Vector2();
	 * const v2 = new Vector2(7, 8);
	 * // change v1 Vector2 to x:7 & y:8
	 * v1.set(v2);
	 * @returns {Vector2}
	 */
	set(...args) {
		const pos = this._checkParams(args);
		this.setX(pos.x);
		this.setY(pos.y);
		return this;
	}

	/**
	 * Setting the x value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	setX(v) {
		this.x = v;
		return this;
	}

	/**
	 * Setting the y value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	setY(v) {
		this.y = v;
		return this;
	}

	/**
	 * Adding values to the the Vector2
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // add  3 to v1 Vector2
	 * v1.add(3);
	 * // v1 values are now x:5 & y:6
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // add 5, 7 to v1 Vector2
	 * v1.add(5, 7);
	 * // v1 values are now x:7 & y:10
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // add  3 to v1 Vector2
	 * v1.add({x:1, y:2});
	 * // v1 values are now x:3 & y:5
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * const v2 = new Vector2(4, 1);
	 * // add  3 to v1 Vector2
	 * v1.add(v2);
	 * // v1 values are now x:6 & y:4, v2 is unchanged
	 * @returns {Vector2}
	 */
	add(...args) {
		const pos = this._checkParams(args);
		this.addX(pos.x);
		this.addY(pos.y);
		return this;
	}

	/**
	 * Adding v to x value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	addX(v) {
		this.x += v;
		return this;
	}


	/**
	 * Adding v to y value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	addY(v) {
		this.y += v;
		return this;
	}


	/**
	 * Substracting values to the the Vector2
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Substracting  3 to v1 Vector2
	 * v1.substract(3);
	 * // v1 values are now x:-1 & y:0
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Substracting 5, 7 to v1 Vector2
	 * v1.substract(5, 7);
	 * // v1 values are now x:-3 & y:-4
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Substracting  3 to v1 Vector2
	 * v1.substract({x:1, y:2});
	 * // v1 values are now x:1 & y:1
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * const v2 = new Vector2(4, 1);
	 * // Substracting 3 to v1 Vector2
	 * v1.substract(v2);
	 * // v1 values are now x:-2 & y:2, v2 is unchanged
	 * @returns {Vector2}
	 */
	substract(...args) {
		const pos = this._checkParams(args);
		this.substractX(pos.x);
		this.substractY(pos.y);
		return this;
	}

	/**
	 * Substracting v to x value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	substractX(v) {
		this.x -= v;
		return this;
	}

	/**
	 * Substracting v to x value of the Vector2
	 * @param {Number} v The value
	 * @returns {Vector2}
	 */
	substractY(v) {
		this.y -= v;
		return this;
	}

	/**
	 * Multiply values to the the Vector2
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Multiply 3 to v1 Vector2
	 * v1.multiply(3);
	 * // v1 values are now x:6 & y:9
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Multiply 5, 7 to v1 Vector2
	 * v1.multiply(5, 7);
	 * // v1 values are now x:10 & y:21
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * // Multiply  1, 2 to v1 Vector2
	 * v1.multiply({x:1, y:2});
	 * // v1 values are now x:2 & y:6
	 * @example
	 * const v1 = new Vector2(2, 3);
	 * const v2 = new Vector2(4, 1);
	 * // Multiply v2 to v1 Vector2
	 * v1.multiply(v2);
	 * // v1 values are now x:8 & y:3, v2 is unchanged
	 * @returns {Vector2}
	 */
	multiply(...args) {
		const pos = this._checkParams(args);
		this.multiplyX(pos.x);
		this.multiplyY(pos.y);
		return this;
	}

	/**
	 * Multiply x value of the Vector2 by v
	 * @param {Number} v The multiplicator
	 * @returns {Vector2}
	 */
	multiplyX(v) {
		this.x *= v;
		return this;
	}


	/**
	 * Multiply y value of the Vector2 by v
	 * @param {Number} v The multiplicator
	 * @returns {Vector2}
	 */
	multiplyY(v) {
		this.y *= v;
		return this;
	}


	/**
	 * Dividing values to the the Vector2
	 * @param {Vector2|Number} p1 Vector2: copy, Number: assign
	 * @param {Number} p2 Number: assign
	 * @example
	 * const v1 = new Vector2(12, 9);
	 * // Dividing v1 Vector2 by 3
	 * v1.divide(3);
	 * // v1 values are now x:4 & y:3
	 * @example
	 * const v1 = new Vector2(10, 15);
	 * // Dividing v1 Vector2 by 2, 5
	 * v1.divide(2, 5);
	 * // v1 values are now x:5 & y:3
	 * @example
	 * const v1 = new Vector2(5, 8);
	 * // Dividing v1 Vector2 by 2, 4
	 * v1.divide({x:2, y:4});
	 * // v1 values are now x:2.5 & y:2
	 * @example
	 * const v1 = new Vector2(12, 10);
	 * const v2 = new Vector2(6, 2);
	 * // Dividing v1 Vector2 by 6, 2
	 * v1.divide(v2);
	 * // v1 values are now x:2 & y:5, v2 is unchanged
	 * @returns {Vector2}
	 */
	divide(...args) {
		const pos = this._checkParams(args);
		this.divideX(pos.x);
		this.divideY(pos.y);
		return this;
	}

	/**
	 * Divinding x value of the Vector2 by v
	 * @param {Number} v The divisor
	 * @returns {Vector2}
	 */
	divideX(v) {
		this.x /= v;
		return this;
	}

	/**
	 * Divinding x value of the Vector2 by v
	 * @param {Number} v The divisor
	 * @returns {Vector2}
	 */
	divideY(v) {
		this.y /= v;
		return this;
	}


	/**
	 * Inverting the Vector2
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * v1.invert();
	 * // v1 values are now x:-3 & y:-6
	 * @returns {Vector2}
	 */
	invert() {
		this.invertX();
		this.invertY();
		return this;
	}

	/**
	 * Inverting the x value of the Vector2
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * v1.invertX();
	 * // v1 values are now x:-3 & y:6
	 * @returns {Vector2}
	 */
	invertX() {
		this.x = 0 - this.x;
		return this;
	}

	/**
	 * Inverting the y value of the Vector2
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * v1.invertY();
	 * // v1 values are now x:3 & y:-6
	 * @returns {Vector2}
	 */
	invertY() {
		this.y = 0 - this.y;
		return this;
	}


	/**
	 * Rotate the Vector2 arround zero by angle
	 * @param {Number} angle degrees angle
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * v1.rotate(180);
	 * // v1 values are now x:-3 & y:-6
	 * @returns {Vector2}
	 */
	rotate(angle) {
		angle = angle * (Math.PI / 180);
		const cos = Math.cos(angle);
		const sin = Math.sin(angle);
		const x = this.x * 1;
		const y = this.y * 1;
		this.x = ((x * cos) - (y * sin));
		this.y = ((x * sin) + (y * cos));
		this.approximate();
		return this;
	}

	/**
	 * Test if the Vector2 is equal to an other
	 * @param {Vector2|Number} [p1] Vector2: copy, Number: assign
	 * @param {Number} [p2] Number: assign
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * const v2 = new Vector2(4, 6);
	 * return v1.equals(v2); // false
	 * @returns {Boolean}
	 */
	equals(...args) {
		const pos = this._checkParams(args);
		return this.equalsX(pos.x) && this.equalsY(pos.y);
	}

	/**
	 * Test if the x value of the Vector2 is equal to an other
	 * @param {Number} v The value to compare
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * const v2 = new Vector2(4, 6);
	 * return v1.equalsX(v2); // false
	 * @returns {Boolean}
	 */
	equalsX(v) {
		return this.x === v;
	}

	/**
	 * Test if the y value of the Vector2 is equal to an other
	 * @param {Number} v The value to compare
	 * @example
	 * const v1 = new Vector2(3, 6);
	 * const v2 = new Vector2(4, 6);
	 * return v1.equalsY(v2); // true
	 * @returns {Boolean}
	 */
	equalsY(v) {
		return this.y === v;
	}

	/**
	 * Test if the current Vector2 is inside a polygon
	 * @param {Polygon} polygon The polygon to check
	 * @example
	 * const polygon = new Polygon([
	 * 	new Vector2(0,0),
	 * 	new Vector2(10,0),
	 * 	new Vector2(10,10),
	 * 	new Vector2(0,10)
	 * ]);
	 * const v1 = new Vector2(5, 5);
	 * return v1.isInside(polygon); // true
	 * @returns {Boolean}
	 */
	isInside(polygon) {
		return polygon.pointIsInside(this);
	}

	/**
	 * @see {@link approximated}
	 * @example
	 * const v1 = new Vector2(Math.PI, Math.PI);
	 * console.log(v1.toString()); // {x: 3.141592653589793, y: 3.141592653589793}
	 * console.log(v1.approximate().toString()); // {x: 3.1416, y: 3.1416}
	 */
	approximate() {
		this.setX(Math.round(this.x * 10000) / 10000).setY(Math.round(this.y * 10000) / 10000);
		return this;
	}

	/**
	 * Approximated value of the Vector2 (used to remove duplicates)
	 * @see {@link approximate}
	 * @type {Vector2}
     * @readonly
	 * @example
	 * const v1 = new Vector2(Math.PI, Math.PI);
	 * console.log(v1.toString()); // {x: 3.141592653589793, y: 3.141592653589793}
	 * console.log(v1.approximate.toString()); // {x: 3.1416, y: 3.1416}
	 */
	get approximated() {
		return this.clone.approximate();
	}

	/**
	 * Normalized Vector2
	 * @see {@link norm}
	 * @type {Vector2}
     * @readonly
	 * @example
	 * const v1 = new Vector2(1, 1);
	 * console.log(v1.normalized.toString()); // {x: 0.7071067811865476, y: 0.7071067811865476}
	 */
	get normalized() {
		return this.divide(this.magnitude);
	}

	/**
	 * Alias of normalized
	 * @see {@link normalized}
     * @readonly
	 * @type {Vector2}
	 */
	get norm() {
		return this.normalized;
	}

	/**
	 * Magnitude of the Vector2
	 * @type {Number}
     * @readonly
	 * @example
	 * const v1 = new Vector2(12, 32);
	 * console.log(v1.magnitude); // 34.17601498127012
	 */
	get magnitude() {
		return Math.sqrt((this.x * this.x) + (this.y * this.y));
	}

	/**
	 * A clone of the Vector2
	 * @type {Vector2}
     * @readonly
	 * @example
	 * const v1 = new Vector2(5, 5);
	 * console.log(v1.clone.add(2, 3)); // {x: 7, y: 8}
	 * // v1 is unchanged
	 */
	get clone() {
		return new Vector2(this.x, this.y);
	}

	/**
	 * Getting JSON string version of the Vector2
	 * @returns {String}
	 */
	toString() {
		return JSON.stringify({
			x: this.x,
			y: this.y
		});
	}

	_checkParams(args) {
		const p1 = args[0];
		const p2 = args[1];
		const pos = {
			x: 0,
			y: 0
		};
		if (p1 !== null && typeof p1 === 'object') {
			if (typeof p1.x !== 'undefined' && typeof p1.y !== 'undefined') {
				pos.x = p1.x * 1;
				pos.y = p1.y * 1;
			}
		} else if (typeof p1 !== 'undefined' && typeof p2 !== 'undefined') {
			pos.x = p1 * 1;
			pos.y = p2 * 1;
		} else if (typeof p1 !== 'undefined') {
			pos.x = p1 * 1;
			pos.y = p1 * 1;
		}
		return pos;
	}

	/**
	 * Getting the distance between two Vector2
	 * @param {Vector2} v1 first Vector2
	 * @param {Vector2} v2 second Vector2
	 * @returns {Number}
	 */
	static Distance(v1, v2) {
		const dx = Math.abs(v1.x - v2.x);
		const dy = Math.abs(v1.y - v2.y);
		return Math.sqrt((dx * dx) + (dy * dy));
	}

	/**
	 * Getting the max values from two Vector2
	 * @param {Vector2} v1 first Vector2
	 * @param {Vector2} v2 second Vector2
	 * @returns {Vector2}
	 */
	static Max(v1, v2) {
		return new Vector2(Math.max(v1.x, v2.x), Math.max(v1.y, v2.y));
	}

	/**
	 * Getting the min values from two Vector2
	 * @param {Vector2} v1 first Vector2
	 * @param {Vector2} v2 second Vector2
	 * @returns {Vector2}
	 */
	static Min(v1, v2) {
		return new Vector2(Math.min(v1.x, v2.x), Math.min(v1.y, v2.y));
	}

	/**
	 * Getting a Lerp Vector2 from v1 to v2 with t time
	 * @param {Vector2} v1 first Vector2
	 * @param {Vector2} v2 second Vector2
	 * @param {Number} t time of lerp, value should be between 0 and 1
	 * @returns {Vector2}
	 */
	static Lerp(v1, v2, t) {
		if (t > 1) {
			t = 1;
		} else if (t < 0) {
			t = 0;
		}
		return Vector2.LerpUnclamped(v1, v2, t);
	}

	/**
	 * Getting a Lerp Vector2 from v1 to v2 with t time (Unclamped version)
	 * @param {Vector2} v1 first Vector2
	 * @param {Vector2} v2 second Vector2
	 * @param {Number} t time of lerp, value should be between 0 and 1, but it can exceed this values
	 * @returns {Vector2}
	 */
	static LerpUnclamped(v1, v2, t) {
		const diffV = v2.clone.substract(v1).multiply(t);
		return v1.clone.add(diffV);
	}

	/**
	 * Test if the passed element is a Vector2 like and return Vector2 corresponding, or false
	 * @param {any} subject The subject to test
	 * @returns {Vector2|false} false on failure
	 */
	static IsVector2Like(subject) {
		if (
			typeof subject === 'object' &&
			typeof subject.x !== 'undefined' &&
			typeof subject.y !== 'undefined'
		) {
			if (subject instanceof Vector2) {
				return subject;
			} else {
				return new Vector2(subject);
			}
		}
		return false;
	}
}

/**
 * A zero Vector2
 * @returns {Vector2}
 */
Vector2.Zero = function() {
	return new Vector2(0, 0);
};

/**
 * A top Vector2
 * @returns {Vector2}
 */
Vector2.Top = function() {
	return new Vector2(0, -1);
};

/**
 * A left Vector2
 * @returns {Vector2}
 */
Vector2.Left = function() {
	return new Vector2(-1, 0);
};

/**
 * A bottom Vector2
 * @returns {Vector2}
 */
Vector2.Bottom = function() {
	return new Vector2(0, 1);
};

/**
 * A right Vector2
 * @returns {Vector2}
 */
Vector2.Right = function() {
	return new Vector2(1, 0);
};

module.exports = Vector2;