Source: math/Quaternion.js

import { Matrix4 } from './Matrix4.js';

/**
 * a Quaternion class
 * @constructor
 * @memberof zen3d
 * @param {number} x
 * @param {number} y
 * @param {number} z
 * @param {number} w
 */
function Quaternion(x, y, z, w) {
	this._x = x || 0;
	this._y = y || 0;
	this._z = z || 0;
	this._w = (w !== undefined) ? w : 1;
}

Object.defineProperties(Quaternion.prototype, {
	x: {
		get: function() {
			return this._x;
		},
		set: function(value) {
			this._x = value;
			this.onChangeCallback();
		}
	},
	y: {
		get: function() {
			return this._y;
		},
		set: function(value) {
			this._y = value;
			this.onChangeCallback();
		}
	},
	z: {
		get: function() {
			return this._z;
		},
		set: function(value) {
			this._z = value;
			this.onChangeCallback();
		}
	},
	w: {
		get: function() {
			return this._w;
		},
		set: function(value) {
			this._w = value;
			this.onChangeCallback();
		}
	}
});

Object.assign(Quaternion.prototype, /** @lends zen3d.Quaternion.prototype */{

	/**
     *
     */
	normalize: function(thickness) {
		var l = this.length();

		if (l === 0) {
			this._x = 0;
			this._y = 0;
			this._z = 0;
			this._w = 1;
		} else {
			l = 1 / l;

			this._x = this._x * l;
			this._y = this._y * l;
			this._z = this._z * l;
			this._w = this._w * l;
		}

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	length: function () {
		return Math.sqrt(this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w);
	},

	/**
     * Linearly interpolates between two quaternions.
     */
	lerpQuaternions: function(q1, q2, ratio) {
		var w1 = q1._w, x1 = q1._x, y1 = q1._y, z1 = q1._z;
		var w2 = q2._w, x2 = q2._x, y2 = q2._y, z2 = q2._z;
		var dot = w1 * w2 + x1 * x2 + y1 * y2 + z1 * z2;

		// shortest direction
		if (dot < 0) {
			w2 = -w2;
			x2 = -x2;
			y2 = -y2;
			z2 = -z2;
		}

		this._w = w1 + ratio * (w2 - w1);
		this._x = x1 + ratio * (x2 - x1);
		this._y = y1 + ratio * (y2 - y1);
		this._z = z1 + ratio * (z2 - z1);
		var len = 1.0 / Math.sqrt(this._w * this._w + this._x * this._x + this._y * this._y + this._z * this._z);
		this._w *= len;
		this._x *= len;
		this._y *= len;
		this._z *= len;

		this.onChangeCallback();

		return this;
	},

	/**
     * Spherically interpolates between two quaternions
     * providing an interpolation between rotations with constant angle change rate.
     */
	slerpQuaternions: function(q1, q2, ratio) {
		var w1 = q1._w, x1 = q1._x, y1 = q1._y, z1 = q1._z;
		var w2 = q2._w, x2 = q2._x, y2 = q2._y, z2 = q2._z;
		var dot = w1 * w2 + x1 * x2 + y1 * y2 + z1 * z2;

		// shortest direction
		if (dot < 0) {
			dot = -dot;
			w2 = -w2;
			x2 = -x2;
			y2 = -y2;
			z2 = -z2;
		}

		if (dot < 0.95) {
			var angle = Math.acos(dot);
			var s = 1 / Math.sin(angle);
			var s1 = Math.sin(angle * (1 - ratio)) * s;
			var s2 = Math.sin(angle * ratio) * s;
			this._w = w1 * s1 + w2 * s2;
			this._x = x1 * s1 + x2 * s2;
			this._y = y1 * s1 + y2 * s2;
			this._z = z1 * s1 + z2 * s2;
		} else {
			// nearly identical angle, interpolate linearly
			this._w = w1 + ratio * (w2 - w1);
			this._x = x1 + ratio * (x2 - x1);
			this._y = y1 + ratio * (y2 - y1);
			this._z = z1 + ratio * (z2 - z1);
			var len = 1.0 / Math.sqrt(this._w * this._w + this._x * this._x + this._y * this._y + this._z * this._z);
			this._w *= len;
			this._x *= len;
			this._y *= len;
			this._z *= len;
		}

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	set: function(x, y, z, w) {
		this._x = x || 0;
		this._y = y || 0;
		this._z = z || 0;
		this._w = (w !== undefined) ? w : 1;

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	copy: function(v) {
		this._x = v.x;
		this._y = v.y;
		this._z = v.z;
		this._w = (v.w !== undefined) ? v.w : 1;

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	setFromEuler: function(euler, update) {
		var c1 = Math.cos(euler._x / 2);
		var c2 = Math.cos(euler._y / 2);
		var c3 = Math.cos(euler._z / 2);
		var s1 = Math.sin(euler._x / 2);
		var s2 = Math.sin(euler._y / 2);
		var s3 = Math.sin(euler._z / 2);

		var order = euler._order;

		if (order === 'XYZ') {
			this._x = s1 * c2 * c3 + c1 * s2 * s3;
			this._y = c1 * s2 * c3 - s1 * c2 * s3;
			this._z = c1 * c2 * s3 + s1 * s2 * c3;
			this._w = c1 * c2 * c3 - s1 * s2 * s3;
		} else if (order === 'YXZ') {
			this._x = s1 * c2 * c3 + c1 * s2 * s3;
			this._y = c1 * s2 * c3 - s1 * c2 * s3;
			this._z = c1 * c2 * s3 - s1 * s2 * c3;
			this._w = c1 * c2 * c3 + s1 * s2 * s3;
		} else if (order === 'ZXY') {
			this._x = s1 * c2 * c3 - c1 * s2 * s3;
			this._y = c1 * s2 * c3 + s1 * c2 * s3;
			this._z = c1 * c2 * s3 + s1 * s2 * c3;
			this._w = c1 * c2 * c3 - s1 * s2 * s3;
		} else if (order === 'ZYX') {
			this._x = s1 * c2 * c3 - c1 * s2 * s3;
			this._y = c1 * s2 * c3 + s1 * c2 * s3;
			this._z = c1 * c2 * s3 - s1 * s2 * c3;
			this._w = c1 * c2 * c3 + s1 * s2 * s3;
		} else if (order === 'YZX') {
			this._x = s1 * c2 * c3 + c1 * s2 * s3;
			this._y = c1 * s2 * c3 + s1 * c2 * s3;
			this._z = c1 * c2 * s3 - s1 * s2 * c3;
			this._w = c1 * c2 * c3 - s1 * s2 * s3;
		} else if (order === 'XZY') {
			this._x = s1 * c2 * c3 - c1 * s2 * s3;
			this._y = c1 * s2 * c3 - s1 * c2 * s3;
			this._z = c1 * c2 * s3 + s1 * s2 * c3;
			this._w = c1 * c2 * c3 + s1 * s2 * s3;
		}

		if (update !== false) this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	setFromRotationMatrix: function (m) {
		var te = m.elements,

			m11 = te[0], m12 = te[4], m13 = te[8],
			m21 = te[1], m22 = te[5], m23 = te[9],
			m31 = te[2], m32 = te[6], m33 = te[10],

			trace = m11 + m22 + m33,
			s;

		if (trace > 0) {
			s = 0.5 / Math.sqrt(trace + 1.0);

			this._w = 0.25 / s;
			this._x = (m32 - m23) * s;
			this._y = (m13 - m31) * s;
			this._z = (m21 - m12) * s;
		} else if (m11 > m22 && m11 > m33) {
			s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33);

			this._w = (m32 - m23) / s;
			this._x = 0.25 * s;
			this._y = (m12 + m21) / s;
			this._z = (m13 + m31) / s;
		} else if (m22 > m33) {
			s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33);

			this._w = (m13 - m31) / s;
			this._x = (m12 + m21) / s;
			this._y = 0.25 * s;
			this._z = (m23 + m32) / s;
		} else {
			s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22);

			this._w = (m21 - m12) / s;
			this._x = (m13 + m31) / s;
			this._y = (m23 + m32) / s;
			this._z = 0.25 * s;
		}

		this.onChangeCallback();

		return this;
	},

	/**
     * @method
     */
	setFromUnitVectors: function (vFrom, vTo) {
		// http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final

		// assumes direction vectors vFrom and vTo are normalized

		var EPS = 0.000001;

		var r = vFrom.dot(vTo) + 1;

		if (r < EPS) {
			r = 0;

			if (Math.abs(vFrom.x) > Math.abs(vFrom.z)) {
				this._x = -vFrom.y;
				this._y = vFrom.x;
				this._z = 0;
				this._w = r;
			} else {
				this._x = 0;
				this._y = -vFrom.z;
				this._z = vFrom.y;
				this._w = r;
			}
		} else {
			// crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3

			this._x = vFrom.y * vTo.z - vFrom.z * vTo.y;
			this._y = vFrom.z * vTo.x - vFrom.x * vTo.z;
			this._z = vFrom.x * vTo.y - vFrom.y * vTo.x;
			this._w = r;
		}

		return this.normalize();
	},

	/**
     *
     */
	multiply: function (q) {
		return this.multiplyQuaternions(this, q);
	},

	/**
     *
     */
	premultiply: function (q) {
		return this.multiplyQuaternions(q, this);
	},

	/**
     *
     */
	multiplyQuaternions: function (a, b) {
		// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm

		var qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
		var qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;

		this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
		this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
		this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
		this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	toMatrix4: function(target) {
		if (!target) {
			target = new Matrix4();
		}
		var ele = target.elements;

		var xy2 = 2.0 * this._x * this._y, xz2 = 2.0 * this._x * this._z, xw2 = 2.0 * this._x * this._w;
		var yz2 = 2.0 * this._y * this._z, yw2 = 2.0 * this._y * this._w, zw2 = 2.0 * this._z * this._w;
		var xx = this._x * this._x, yy = this._y * this._y, zz = this._z * this._z, ww = this._w * this._w;

		ele[0] = xx - yy - zz + ww;
		ele[4] = xy2 - zw2;
		ele[8] = xz2 + yw2;
		ele[12] = 0;
		ele[1] = xy2 + zw2;
		ele[5] = -xx + yy - zz + ww;
		ele[9] = yz2 - xw2;
		ele[13] = 0;
		ele[2] = xz2 - yw2;
		ele[6] = yz2 + xw2;
		ele[10] = -xx - yy + zz + ww;
		ele[14] = 0;
		ele[3] = 0.0;
		ele[7] = 0.0;
		ele[11] = 0;
		ele[15] = 1;

		return target;
	},

	/**
     *
     */
	dot: function (v) {
		return this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w;
	},

	/**
     * Set quaternion from axis angle
     */
	setFromAxisAngle: function(axis, angle) {
		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm

		// assumes axis is normalized

		var halfAngle = angle / 2, s = Math.sin(halfAngle);

		this._x = axis.x * s;
		this._y = axis.y * s;
		this._z = axis.z * s;
		this._w = Math.cos(halfAngle);

		this.onChangeCallback();

		return this;
	},

	/**
     *
     */
	fromArray: function (array, offset) {
		if (offset === undefined) offset = 0;

		this._x = array[offset];
		this._y = array[offset + 1];
		this._z = array[offset + 2];
		this._w = array[offset + 3];

		this.onChangeCallback();

		return this;
	},

	toArray: function (array, offset) {
		if (array === undefined) array = [];
		if (offset === undefined) offset = 0;

		array[offset] = this._x;
		array[offset + 1] = this._y;
		array[offset + 2] = this._z;
		array[offset + 3] = this._w;

		return array;
	},

	onChange: function(callback) {
		this.onChangeCallback = callback;

		return this;
	},

	onChangeCallback: function() {}

});

Object.assign(Quaternion, {

	/**
     * @memberof zen3d.Quaternion
     */
	slerpFlat: function (dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t) {
		// fuzz-free, array-based Quaternion SLERP operation

		var x0 = src0[srcOffset0 + 0],
			y0 = src0[srcOffset0 + 1],
			z0 = src0[srcOffset0 + 2],
			w0 = src0[srcOffset0 + 3],

			x1 = src1[srcOffset1 + 0],
			y1 = src1[srcOffset1 + 1],
			z1 = src1[srcOffset1 + 2],
			w1 = src1[srcOffset1 + 3];

		if (w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1) {
			var s = 1 - t,

				cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1,

				dir = (cos >= 0 ? 1 : -1),
				sqrSin = 1 - cos * cos;

			// Skip the Slerp for tiny steps to avoid numeric problems:
			if (sqrSin > Number.EPSILON) {
				var sin = Math.sqrt(sqrSin),
					len = Math.atan2(sin, cos * dir);

				s = Math.sin(s * len) / sin;
				t = Math.sin(t * len) / sin;
			}

			var tDir = t * dir;

			x0 = x0 * s + x1 * tDir;
			y0 = y0 * s + y1 * tDir;
			z0 = z0 * s + z1 * tDir;
			w0 = w0 * s + w1 * tDir;

			// Normalize in case we just did a lerp:
			if (s === 1 - t) {
				var f = 1 / Math.sqrt(x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0);

				x0 *= f;
				y0 *= f;
				z0 *= f;
				w0 *= f;
			}
		}

		dst[dstOffset] = x0;
		dst[dstOffset + 1] = y0;
		dst[dstOffset + 2] = z0;
		dst[dstOffset + 3] = w0;
	}

});

export { Quaternion };