// TODO: Type errors if v not instanceof Vector classes
// TODO: Possibly implement in C++

/**
 * @class Vector2D
 * @brief Class for representing and manipulating 2D vectors
 *
 * @param {number} x First coordinate value, defaults to 0.
 * @param {number} y Second coordinate value, defaults to 0.
 */
function Vector2D(x = 0, y = 0)
{
	this.set(x, y);
}

Vector2D.prototype.clone = function()
{
	return new Vector2D(this.x, this.y);
};

// Mutating 2D functions
//
// These functions modify the current object,
// and always return this object to allow chaining

Vector2D.prototype.set = function(x, y)
{
	this.x = x;
	this.y = y;
	return this;
};

Vector2D.prototype.setFrom = function(v)
{
	this.x = v.x;
	this.y = v.y;
	return this;
};

Vector2D.prototype.add = function(v)
{
	this.x += v.x;
	this.y += v.y;
	return this;
};

Vector2D.prototype.sub = function(v)
{
	this.x -= v.x;
	this.y -= v.y;
	return this;
};

Vector2D.prototype.mult = function(f)
{
	this.x *= f;
	this.y *= f;
	return this;
};

Vector2D.prototype.div = function(f)
{
	this.x /= f;
	this.y /= f;
	return this;
};

Vector2D.prototype.normalize = function()
{
	const magnitude = this.length();
	if (!magnitude)
		return this;

	return this.div(magnitude);
};

/**
 * Rotate a radians anti-clockwise
 */
Vector2D.prototype.rotate = function(angle)
{
	const sin = Math.sin(angle);
	const cos = Math.cos(angle);

	return this.set(
		this.x * cos + this.y * sin,
		-this.x * sin + this.y * cos);
};

/**
 * Rotate radians anti-clockwise around the specified rotation center.
 */
Vector2D.prototype.rotateAround = function(angle, center)
{
	return this.sub(center).rotate(angle).add(center);
};

/**
 * Convert to integer coordinates.
 */
Vector2D.prototype.round = function()
{
	return this.set(Math.round(this.x), Math.round(this.y));
};

Vector2D.prototype.floor = function()
{
	return this.set(Math.floor(this.x), Math.floor(this.y));
};

Vector2D.prototype.toFixed = function(digits)
{
	return this.set(this.x.toFixed(digits), this.y.toFixed(digits));
};

// Numeric 2D info functions (non-mutating)
//
// These methods serve to get numeric info on the vector, they don't modify the vector

/**
 * Returns a vector that forms a right angle with this one.
 */
Vector2D.prototype.perpendicular = function()
{
	return new Vector2D(-this.y, this.x);
};

/**
 * Computes the scalar product of the two vectors.
 * Geometrically, this is the product of the length of the two vectors and the cosine of the angle between them.
 * If the vectors are orthogonal, the product is zero.
 */
Vector2D.prototype.dot = function(v)
{
	return this.x * v.x + this.y * v.y;
};

/**
 * Computes the non-zero coordinate of the cross product of the two vectors.
 * Geometrically, the cross of the vectors is a 3D vector perpendicular to the two 2D vectors.
 * The returned number corresponds to the area of the parallelogram with the vectors for sides.
 */
Vector2D.prototype.cross = function(v)
{
	return this.x * v.y - this.y * v.x;
};

Vector2D.prototype.lengthSquared = function()
{
	return this.dot(this);
};

Vector2D.prototype.length = function()
{
	return Math.sqrt(this.lengthSquared());
};

/**
 * Compare this length to the length of v.
 * @return 0 if the lengths are equal
 * @return 1 if this is longer than v
 * @return -1 if this is shorter than v
 * @return NaN if the vectors aren't comparable
 */
Vector2D.prototype.compareLength = function(v)
{
	return Math.sign(this.lengthSquared() - v.lengthSquared());
};

Vector2D.prototype.distanceToSquared = function(v)
{
	return Math.euclidDistance2DSquared(this.x, this.y, v.x, v.y);
};

Vector2D.prototype.distanceTo = function(v)
{
	return Math.euclidDistance2D(this.x, this.y, v.x, v.y);
};

/**
 * Returns the angle going from this position to v.
 * Angles are between -PI and PI. E.g., north is 0, east is PI/2.
 */
Vector2D.prototype.angleTo = function(v)
{
	return Math.atan2(v.x - this.x, v.y - this.y);
};

// Static 2D functions
//
// Static functions that return a new vector object.
// Note that object creation is slow in JS, so use them only when necessary

Vector2D.from3D = function(v)
{
	return new Vector2D(v.x, v.z);
};

Vector2D.add = function(v1, v2)
{
	return new Vector2D(v1.x + v2.x, v1.y + v2.y);
};

Vector2D.sub = function(v1, v2)
{
	return new Vector2D(v1.x - v2.x, v1.y - v2.y);
};

Vector2D.isEqualTo = function(v1, v2)
{
	return v1.x == v2.x && v1.y == v2.y;
};

Vector2D.mult = function(v, f)
{
	return new Vector2D(v.x * f, v.y * f);
};

Vector2D.div = function(v, f)
{
	return new Vector2D(v.x / f, v.y / f);
};

Vector2D.min = function(v1, v2)
{
	return new Vector2D(Math.min(v1.x, v2.x), Math.min(v1.y, v2.y));
};

Vector2D.max = function(v1, v2)
{
	return new Vector2D(Math.max(v1.x, v2.x), Math.max(v1.y, v2.y));
};

Vector2D.average = function(vectorList)
{
	return Vector2D.sum(vectorList).div(vectorList.length);
};

Vector2D.sum = function(vectorList)
{
	// Do not use for...of nor array functions for performance
	const sum = new Vector2D();

	for (let i = 0; i < vectorList.length; ++i)
		sum.add(vectorList[i]);

	return sum;
};

Vector2D.dot = function(v1, v2)
{
	return v1.x * v2.x + v1.y * v2.y;
};

/**
 * @class Vector3D
 * @brief Class for representing and manipulating 3D vectors
 *
 * @param {number} x First coordinate value, defaults to 0.
 * @param {number} y Second coordinate value, defaults to 0.
 * @param {number} z Third coordinate value, defaults to 0.
 */
function Vector3D(x = 0, y = 0, z = 0)
{
	this.set(x, y, z);
}

Vector3D.prototype.clone = function()
{
	return new Vector3D(this.x, this.y, this.z);
};

// Mutating 3D functions
//
// These functions modify the current object,
// and always return this object to allow chaining

Vector3D.prototype.set = function(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
	return this;
};

Vector3D.prototype.add = function(v)
{
	this.x += v.x;
	this.y += v.y;
	this.z += v.z;
	return this;
};

Vector3D.prototype.sub = function(v)
{
	this.x -= v.x;
	this.y -= v.y;
	this.z -= v.z;
	return this;
};

Vector3D.prototype.mult = function(f)
{
	this.x *= f;
	this.y *= f;
	this.z *= f;
	return this;
};

Vector3D.prototype.div = function(f)
{
	this.x /= f;
	this.y /= f;
	this.z /= f;
	return this;
};

Vector3D.prototype.normalize = function()
{
	const magnitude = this.length();
	if (!magnitude)
		return this;

	return this.div(magnitude);
};

/**
 * Convert to integer coordinates.
 */
Vector3D.prototype.round = function()
{
	return this.set(Math.round(this.x), Math.round(this.y), Math.round(this.z));
};

Vector3D.prototype.floor = function()
{
	return this.set(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));
};

Vector3D.prototype.toFixed = function(digits)
{
	return this.set(this.x.toFixed(digits), this.y.toFixed(digits), this.z.toFixed(digits));
};

// Numeric 3D info functions (non-mutating)
//
// These methods serve to get numeric info on the vector, they don't modify the vector

Vector3D.prototype.dot = function(v)
{
	return this.x * v.x + this.y * v.y + this.z * v.z;
};

/**
 * Returns a vector perpendicular to the two given vectors.
 * The length of the returned vector corresponds to the area of the parallelogram with the vectors for sides.
 */
Vector3D.prototype.cross = function(v)
{
	return new Vector3D(
		this.y * v.z - this.z * v.y,
		this.z * v.x - this.x * v.z,
		this.x * v.y - this.y * v.x);
};

Vector3D.prototype.lengthSquared = function()
{
	return this.dot(this);
};

Vector3D.prototype.length = function()
{
	return Math.sqrt(this.lengthSquared());
};

/**
 * Compare this length to the length of v,
 * @return 0 if the lengths are equal
 * @return 1 if this is longer than v
 * @return -1 if this is shorter than v
 * @return NaN if the vectors aren't comparable
 */
Vector3D.prototype.compareLength = function(v)
{
	return Math.sign(this.lengthSquared() - v.lengthSquared());
};

Vector3D.prototype.distanceToSquared = function(v)
{
	return Math.euclidDistance3DSquared(this.x, this.y, this.z, v.x, v.y, v.z);
};

Vector3D.prototype.distanceTo = function(v)
{
	return Math.euclidDistance3D(this.x, this.y, this.z, v.x, v.y, v.z);
};

Vector3D.prototype.horizDistanceToSquared = function(v)
{
	return Math.euclidDistance2DSquared(this.x, this.z, v.x, v.z);
};

Vector3D.prototype.horizDistanceTo = function(v)
{
	return Math.sqrt(this.horizDistanceToSquared(v));
};

/**
 * Returns the angle going from this position to v.
 */
Vector3D.prototype.horizAngleTo = function(v)
{
	return Math.atan2(v.x - this.x, v.z - this.z);
};

// Static 3D functions
//
// Static functions that return a new vector object.
// Note that object creation is slow in JS, so use them only when really necessary

Vector3D.add = function(v1, v2)
{
	return new Vector3D(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
};

Vector3D.sub = function(v1, v2)
{
	return new Vector3D(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
};

Vector3D.isEqualTo = function(v1, v2)
{
	return v1.x == v2.x && v1.y == v2.y && v1.z == v2.z;
};

Vector3D.mult = function(v, f)
{
	return new Vector3D(v.x * f, v.y * f, v.z * f);
};

Vector3D.div = function(v, f)
{
	return new Vector3D(v.x / f, v.y / f, v.z / f);
};
