import { assert } from "../assert.js";
import { combine_hash } from "../collection/array/combine_hash.js";
import { Signal } from "../events/signal/Signal.js";
import { lerp } from "../math/lerp.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";

/**
 * Class representing a 4D vector. A 4D vector is an ordered quadruplet of numbers (labeled x, y, z, and w), which can be used to represent a number of things, such as:
 *
 * A point in 4D space.
 * A direction and length in 4D space. The length will always be the Euclidean distance (straight-line distance) from `(0, 0, 0, 0)` to `(x, y, z, w)` and the direction is also measured from `(0, 0, 0, 0)` towards `(x, y, z, w)`.
 * Any arbitrary ordered quadruplet of numbers.
 * There are other things a 4D vector can be used to represent, however these are the most common uses.
 *
 * Iterating through a Vector4 instance will yield its components (x, y, z, w) in the corresponding order.
 *
 * Backed by a `Float64Array` of length 4, so instances are usable directly as typed-array views and indexed access `v[0]`..`v[3]` aliases `v.x`..`v.w`.
 *
 * @implements Iterable<number>
 */
export class Vector4 extends Float64Array {
    /**
     *
     * @param {number} [x=0]
     * @param {number} [y=0]
     * @param {number} [z=0]
     * @param {number} [w=0]
     * @constructor
     * @class
     */
    constructor(
        x = 0,
        y = 0,
        z = 0,
        w = 0
    ) {
        super(4);

        this[0] = x;
        this[1] = y;
        this[2] = z;
        this[3] = w;

        /**
         * Dispatched when the value changes
         * @readonly
         * @type {Signal<number,number,number,number,number,number,number,number>}
         */
        this.onChanged = new Signal();
    }

    /**
     * Ensure that built-in `TypedArray` methods (`map`, `slice`, `subarray`, ...) do not
     * try to construct a `Vector4` via the buffer-form constructor.
     * @returns {Float64ArrayConstructor}
     */
    static get [Symbol.species]() {
        return Float64Array;
    }

    /**
     *
     * @returns {number}
     */
    get x() {
        return this[0];
    }

    /**
     *
     * @param {number} v
     */
    set x(v) {
        this[0] = v;
    }

    /**
     *
     * @returns {number}
     */
    get y() {
        return this[1];
    }

    /**
     *
     * @param {number} v
     */
    set y(v) {
        this[1] = v;
    }

    /**
     *
     * @returns {number}
     */
    get z() {
        return this[2];
    }

    /**
     *
     * @param {number} v
     */
    set z(v) {
        this[2] = v;
    }

    /**
     *
     * @returns {number}
     */
    get w() {
        return this[3];
    }

    /**
     *
     * @param {number} v
     */
    set w(v) {
        this[3] = v;
    }

    /**
     *
     * @param {number[]} array
     * @param {number} offset
     */
    readFromArray(array, offset = 0) {
        this.set(
            array[offset],
            array[offset + 1],
            array[offset + 2],
            array[offset + 3]
        );
    }

    /**
     *
     * @param {number[]} array
     * @param {number} offset
     */
    writeToArray(array, offset = 0) {
        array[offset] = this[0];
        array[offset + 1] = this[1];
        array[offset + 2] = this[2];
        array[offset + 3] = this[3];
    }

    /**
     *
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @param {Number} w
     * @returns {Vector4}
     */
    set(x, y, z, w) {
        assert.equal(typeof x, "number", `X must be of type "number", instead was "${typeof x}"`);
        assert.equal(typeof y, "number", `Y must be of type "number", instead was "${typeof y}"`);
        assert.equal(typeof z, "number", `Z must be of type "number", instead was "${typeof z}"`);
        assert.equal(typeof w, "number", `W must be of type "number", instead was "${typeof w}"`);

        assert.ok(!Number.isNaN(x), `X must be a valid number, instead was NaN`);
        assert.ok(!Number.isNaN(y), `Y must be a valid number, instead was NaN`);
        assert.ok(!Number.isNaN(z), `Z must be a valid number, instead was NaN`);
        assert.ok(!Number.isNaN(w), `W must be a valid number, instead was NaN`);

        const _x = this[0];
        const _y = this[1];
        const _z = this[2];
        const _w = this[3];

        if (_x !== x || _y !== y || _z !== z || _w !== w) {

            this[0] = x;
            this[1] = y;
            this[2] = z;
            this[3] = w;

            if (this.onChanged.hasHandlers()) {
                this.onChanged.send8(
                    x, y, z, w,
                    _x, _y, _z, _w
                );
            }
        }

        return this;
    }

    /**
     *
     * @param {Vector3} v3
     */
    multiplyVector3(v3) {
        const x = this[0] * v3.x;
        const y = this[1] * v3.y;
        const z = this[2] * v3.z;

        this.set(x, y, z, this[3]);
    }

    /**
     *
     * @param {Number} value
     * @returns {Vector4}
     */
    multiplyScalar(value) {
        assert.isNumber(value, 'value');
        assert.notNaN(value, 'value');

        return this.set(this[0] * value, this[1] * value, this[2] * value, this[3] * value);
    }

    /**
     *
     * @param {number} a0
     * @param {number} a1
     * @param {number} a2
     * @param {number} a3
     * @param {number} b0
     * @param {number} b1
     * @param {number} b2
     * @param {number} b3
     * @param {number} c0
     * @param {number} c1
     * @param {number} c2
     * @param {number} c3
     * @param {number} d0
     * @param {number} d1
     * @param {number} d2
     * @param {number} d3
     * @returns {Vector4} this
     */
    _applyMatrix4(
        a0, a1, a2, a3,
        b0, b1, b2, b3,
        c0, c1, c2, c3,
        d0, d1, d2, d3
    ) {

        const _x = this[0];
        const _y = this[1];
        const _z = this[2];
        const _w = this[3];

        const x = a0 * _x + b0 * _y + c0 * _z + d0 * _w;
        const y = a1 * _x + b1 * _y + c1 * _z + d1 * _w;
        const z = a2 * _x + b2 * _y + c2 * _z + d2 * _w;
        const w = a3 * _x + b3 * _y + c3 * _z + d3 * _w;

        return this.set(x, y, z, w);
    }

    /**
     *
     * @param {Vector4} other
     * @returns {number}
     */
    dot(other) {
        return this[0] * other.x + this[1] * other.y + this[2] * other.z + this[3] * other.w;
    }

    /**
     * Add XYZ components from another vector
     * @param {Vector3|Vector4} v3
     * @returns {Vector4}
     */
    add3(v3) {
        return this._add(
            v3.x,
             v3.y,
             v3.z,
            0
        );
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @param {number} w
     * @returns {this}
     */
    _add(x,y,z,w){
        return this.set(
            this[0] + x,
            this[1] + y,
            this[2] + z,
            this[3] + w
        );
    }

    /**
     * `this = this + other * scale`
     * @param {Vector4} other
     * @param {number} scale
     * @returns {this}
     */
    addScaled(other, scale){
        return this._add(
            other.x * scale,
            other.y * scale,
            other.z * scale,
            other.w * scale
        );
    }

    /**
     * @deprecated use {@link applyMatrix4} directly instead
     * @param {Matrix4} m
     * @returns {Vector4} this
     */
    threeApplyMatrix4(m) {
        return this.applyMatrix4(m.elements);
    }

    /**
     *
     * @param {number[]|Float32Array} m 4x4 transformation matrix
     * @return {Vector4}
     */
    applyMatrix4(m) {
        return this._applyMatrix4(
            m[0], m[1], m[2], m[3],
            m[4], m[5], m[6], m[7],
            m[8], m[9], m[10], m[11],
            m[12], m[13], m[14], m[15]
        );
    }

    /**
     *
     * @param {Vector4} vec4
     * @returns {Vector4}
     */
    copy(vec4) {
        return this.set(vec4.x, vec4.y, vec4.z, vec4.w);
    }

    /**
     *
     * @returns {Vector4}
     */
    clone() {
        const r = new Vector4();

        r.copy(this);

        return r;
    }

    /**
     *
     * @param {Quaternion} q
     */
    applyQuaternion(q) {
        const x = this[0];
        const y = this[1];
        const z = this[2];
        const w = this[3];

        const qx = q.x;
        const qy = q.y;
        const qz = q.z;
        const qw = q.w;

        // calculate quat * vec
        const ix = qw * x + qy * z - qz * y;
        const iy = qw * y + qz * x - qx * z;
        const iz = qw * z + qx * y - qy * x;
        const iw = -qx * x - qy * y - qz * z;

        // calculate result * inverse quat
        const _x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
        const _y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
        const _z = iz * qw + iw * -qz + ix * -qy - iy * -qx;

        this.set(_x, _y, _z, w);
    }

    /**
     *
     * @param {Vector4} vec4
     * @returns {boolean}
     */
    equals(vec4) {
        return this[0] === vec4.x && this[1] === vec4.y && this[2] === vec4.z && this[3] === vec4.w;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return combine_hash(
            computeHashFloat(this[0]),
            computeHashFloat(this[1]),
            computeHashFloat(this[2]),
            computeHashFloat(this[3])
        );
    }

    /**
     * @param {Vector4} v0
     * @param {Vector4} v1
     * @param {Number} f interpolation fraction
     */
    lerpVectors(v0, v1, f) {
        Vector4.lerp(v0, v1, f, this);
    }

    /**
     *
     * @param {number[]} result
     */
    toArray(result) {
        result[0] = this[0];
        result[1] = this[1];
        result[2] = this[2];
        result[3] = this[3];
    }

    /**
     * @returns {number[]}
     */
    asArray() {
        const result = [];

        this.toArray(result);

        return result;
    }

    /**
     *
     * @param {number[]} data
     * @param {number} offset
     */
    setFromArray(data, offset) {
        this.set(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]);
    }

    toJSON() {
        return {
            x: this[0],
            y: this[1],
            z: this[2],
            w: this[3]
        };
    }

    fromJSON(json) {
        this.copy(json);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        buffer.writeFloat64(this[0]);
        buffer.writeFloat64(this[1]);
        buffer.writeFloat64(this[2]);
        buffer.writeFloat64(this[3]);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        const x = buffer.readFloat64();
        const y = buffer.readFloat64();
        const z = buffer.readFloat64();
        const w = buffer.readFloat64();

        this.set(x, y, z, w);
    }

    /**
     *
     * @param {Vector4} v0
     * @param {Vector4} v1
     * @param {Number} f interpolation fraction
     * @param {Vector4} result
     */
    static lerp(v0, v1, f, result) {
        const x = lerp(v0.x, v1.x, f);
        const y = lerp(v0.y, v1.y, f);
        const z = lerp(v0.z, v1.z, f);
        const w = lerp(v0.w, v1.w, f);

        result.set(x, y, z, w);
    }
}

/**
 * @readonly
 * @type {boolean}
 */
Vector4.prototype.isVector4 = true;

Vector4.prototype.fromArray = Vector4.prototype.readFromArray;
Vector4.prototype.toArray = Vector4.prototype.writeToArray;

/**
 * @readonly
 * @type {string}
 */
Vector4.typeName = "Vector4";

export default Vector4;
