/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Vector handling functions                                          (c) Chris Veness 2011-2019  */
/*                                                                                   MIT Licence  */
/* www.movable-type.co.uk/scripts/geodesy-library.html#vector3d                                   */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */


/**
 * Library of 3-d vector manipulation routines.
 *
 * @module vector3d
 */


/* Vector3d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/**
 * Functions for manipulating generic 3-d vectors.
 *
 * Functions return vectors as return results, so that operations can be chained.
 *
 * @example
 *   const v = v1.cross(v2).dot(v3) // ≡ v1×v2⋅v3
 */
class Vector3d {

    /**
     * Creates a 3-d vector.
     *
     * @param {number} x - X component of vector.
     * @param {number} y - Y component of vector.
     * @param {number} z - Z component of vector.
     *
     * @example
     *   import Vector3d from '/js/geodesy/vector3d.js';
     *   const v = new Vector3d(0.267, 0.535, 0.802);
     */
    constructor(x, y, z) {
        if (isNaN(x) || isNaN(y) || isNaN(z)) throw new TypeError(`invalid vector [${x},${y},${z}]`);

        this.x = Number(x);
        this.y = Number(y);
        this.z = Number(z);
    }


    /**
     * Length (magnitude or norm) of ‘this’ vector.
     *
     * @returns {number} Magnitude of this vector.
     */
    get length() {
        return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
    }


    /**
     * Adds supplied vector to ‘this’ vector.
     *
     * @param   {Vector3d} v - Vector to be added to this vector.
     * @returns {Vector3d} Vector representing sum of this and v.
     */
    plus(v) {
        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');

        return new Vector3d(this.x + v.x, this.y + v.y, this.z + v.z);
    }


    /**
     * Subtracts supplied vector from ‘this’ vector.
     *
     * @param   {Vector3d} v - Vector to be subtracted from this vector.
     * @returns {Vector3d} Vector representing difference between this and v.
     */
    minus(v) {
        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');

        return new Vector3d(this.x - v.x, this.y - v.y, this.z - v.z);
    }


    /**
     * Multiplies ‘this’ vector by a scalar value.
     *
     * @param   {number}   x - Factor to multiply this vector by.
     * @returns {Vector3d} Vector scaled by x.
     */
    times(x) {
        if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);

        return new Vector3d(this.x * x, this.y * x, this.z * x);
    }


    /**
     * Divides ‘this’ vector by a scalar value.
     *
     * @param   {number}   x - Factor to divide this vector by.
     * @returns {Vector3d} Vector divided by x.
     */
    dividedBy(x) {
        if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);

        return new Vector3d(this.x / x, this.y / x, this.z / x);
    }


    /**
     * Multiplies ‘this’ vector by the supplied vector using dot (scalar) product.
     *
     * @param   {Vector3d} v - Vector to be dotted with this vector.
     * @returns {number}   Dot product of ‘this’ and v.
     */
    dot(v) {
        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');

        return this.x * v.x + this.y * v.y + this.z * v.z;
    }


    /**
     * Multiplies ‘this’ vector by the supplied vector using cross (vector) product.
     *
     * @param   {Vector3d} v - Vector to be crossed with this vector.
     * @returns {Vector3d} Cross product of ‘this’ and v.
     */
    cross(v) {
        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');

        const x = this.y * v.z - this.z * v.y;
        const y = this.z * v.x - this.x * v.z;
        const z = this.x * v.y - this.y * v.x;

        return new Vector3d(x, y, z);
    }


    /**
     * Negates a vector to point in the opposite direction.
     *
     * @returns {Vector3d} Negated vector.
     */
    negate() {
        return new Vector3d(-this.x, -this.y, -this.z);
    }


    /**
     * Normalizes a vector to its unit vector
     * – if the vector is already unit or is zero magnitude, this is a no-op.
     *
     * @returns {Vector3d} Normalised version of this vector.
     */
    unit() {
        const norm = this.length;
        if (norm == 1) return this;
        if (norm == 0) return this;

        const x = this.x / norm;
        const y = this.y / norm;
        const z = this.z / norm;

        return new Vector3d(x, y, z);
    }


    /**
     * Calculates the angle between ‘this’ vector and supplied vector atan2(|p₁×p₂|, p₁·p₂) (or if
     * (extra-planar) ‘n’ supplied then atan2(n·p₁×p₂, p₁·p₂).
     *
     * @param   {Vector3d} v - Vector whose angle is to be determined from ‘this’ vector.
     * @param   {Vector3d} [n] - Plane normal: if supplied, angle is signed +ve if this->v is
     *                     clockwise looking along n, -ve in opposite direction.
     * @returns {number}   Angle (in radians) between this vector and supplied vector (in range 0..π
     *                     if n not supplied, range -π..+π if n supplied).
     */
    angleTo(v, n=undefined) {
        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
        if (!(n instanceof Vector3d || n == undefined)) throw new TypeError('n is not Vector3d object');

        // q.v. stackoverflow.com/questions/14066933#answer-16544330, but n·p₁×p₂ is numerically
        // ill-conditioned, so just calculate sign to apply to |p₁×p₂|

        // if n·p₁×p₂ is -ve, negate |p₁×p₂|
        const sign = n==undefined || this.cross(v).dot(n)>=0 ? 1 : -1;

        const sinθ = this.cross(v).length * sign;
        const cosθ = this.dot(v);

        return Math.atan2(sinθ, cosθ);
    }


    /**
     * Rotates ‘this’ point around an axis by a specified angle.
     *
     * @param   {Vector3d} axis - The axis being rotated around.
     * @param   {number}   angle - The angle of rotation (in degrees).
     * @returns {Vector3d} The rotated point.
     */
    rotateAround(axis, angle) {
        if (!(axis instanceof Vector3d)) throw new TypeError('axis is not Vector3d object');

        const θ = angle.toRadians();

        // en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle
        // en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix
        const p = this.unit();
        const a = axis.unit();

        const s = Math.sin(θ);
        const c = Math.cos(θ);
        const t = 1-c;
        const x = a.x, y = a.y, z = a.z;

        const r = [ // rotation matrix for rotation about supplied axis
            [ t*x*x + c,   t*x*y - s*z, t*x*z + s*y ],
            [ t*x*y + s*z, t*y*y + c,   t*y*z - s*x ],
            [ t*x*z - s*y, t*y*z + s*x, t*z*z + c   ],
        ];

        // multiply r × p
        const rp = [
            r[0][0]*p.x + r[0][1]*p.y + r[0][2]*p.z,
            r[1][0]*p.x + r[1][1]*p.y + r[1][2]*p.z,
            r[2][0]*p.x + r[2][1]*p.y + r[2][2]*p.z,
        ];
        const p2 = new Vector3d(rp[0], rp[1], rp[2]);

        return p2;
        // qv en.wikipedia.org/wiki/Rodrigues'_rotation_formula...
    }


    /**
     * String representation of vector.
     *
     * @param   {number} [dp=3] - Number of decimal places to be used.
     * @returns {string} Vector represented as [x,y,z].
     */
    toString(dp=3) {
        return `[${this.x.toFixed(dp)},${this.y.toFixed(dp)},${this.z.toFixed(dp)}]`;
    }

}


// Extend Number object with methods to convert between degrees & radians
Number.prototype.toRadians = function() { return this * Math.PI / 180; };
Number.prototype.toDegrees = function() { return this * 180 / Math.PI; };

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

export default Vector3d;
