import { assert } from "../assert.js";
import Signal from "../events/signal/Signal.js";
import { clamp } from "../math/clamp.js";
import { EPSILON } from "../math/EPSILON.js";
import { epsilonEquals } from "../math/epsilonEquals.js";
import { lerp } from "../math/lerp.js";
import { max2 } from "../math/max2.js";
import { min2 } from "../math/min2.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";
import { v2_distance } from "./vec2/v2_distance.js";
import { v2_dot } from "./vec2/v2_dot.js";
import { v2_length } from "./vec2/v2_length.js";
import { v2_length_sqr } from "./vec2/v2_length_sqr.js";

/**
 * Class representing a 2D vector. A 2D vector is an ordered pair of numbers (labeled x and y), which can be used to represent a number of things, such as:
 *
 * A point in 2D space (i.e. a position on a plane).
 * A direction and length across a plane. The length will always be the Euclidean distance (straight-line distance) from `(0, 0)` to `(x, y)` and the direction is also measured from `(0, 0)` towards `(x, y)`.
 * Any arbitrary ordered pair of numbers.
 * There are other things a 2D vector can be used to represent, such as momentum vectors, complex numbers and so on, however these are the most common uses.
 *
 * Iterating through a Vector2 instance will yield its components `(x, y)` in the corresponding order.
 *
 * Backed by a `Float64Array` of length 2, so instances are usable directly as typed-array views and indexed access `v[0]`, `v[1]` aliases `v.x`, `v.y`.
 *
 * @implements Iterable<number>
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Vector2 extends Float64Array {
    /**
     *
     * @param {number} [x=0]
     * @param {number} [y=0]
     */
    constructor(x = 0, y = 0) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');

        assert.notNaN(x, 'x');
        assert.notNaN(y, 'y');

        super(2);

        this[0] = x;
        this[1] = y;

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

    /**
     * Ensure that built-in `TypedArray` methods (`map`, `slice`, `subarray`, ...) do not
     * try to construct a `Vector2` 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;
    }

    /**
     *
     * @param {number[]|Float32Array} array
     * @param {number} [offset]
     * @returns {this}
     */
    fromArray(array, offset = 0) {
        return this.set(
            array[offset],
            array[offset + 1]
        );
    }

    /**
     *
     * @param {number[]|Float32Array} array
     * @param {number} [offset]
     */
    toArray(array, offset = 0) {
        assert.isArrayLike(array, 'array');
        assert.isNonNegativeInteger(offset, 'offset');

        array[offset] = this[0];
        array[offset + 1] = this[1];
    }

    asArray() {
        const r = [];

        this.toArray(r, 0);

        return r;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {this}
     */
    set(x, y) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');

        assert.notNaN(x, 'x');
        assert.notNaN(y, 'y');

        const oldX = this[0];
        const oldY = this[1];

        if (oldX !== x || oldY !== y) {
            this[0] = x;
            this[1] = y;

            if (this.onChanged.hasHandlers()) {
                this.onChanged.send4(x, y, oldX, oldY);
            }

        }

        return this;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {this}
     */
    setSilent(x, y) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');

        assert.notNaN(x, 'x');
        assert.notNaN(y, 'y');

        this[0] = x;
        this[1] = y;

        return this;
    }

    /**
     *
     * @param {number} x
     * @returns {this}
     */
    setX(x) {
        return this.set(x, this[1]);
    }

    /**
     *
     * @param {number} y
     * @returns {this}
     */
    setY(y) {
        return this.set(this[0], y);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {this}
     */
    _sub(x, y) {
        return this.set(this[0] - x, this[1] - y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    sub(other) {
        return this._sub(other.x, other.y);
    }

    /**
     *
     * @param {Vector2} a
     * @param {Vector2} b
     * @returns {this}
     */
    subVectors(a, b) {
        return this.set(a.x - b.x, a.y - b.y);
    }

    /**
     * performs Math.floor operation on x and y
     * @returns {this}
     */
    floor() {
        return this.set(Math.floor(this[0]), Math.floor(this[1]));
    }

    /**
     * performs Math.ceil operation on x and y
     * @returns {this}
     */
    ceil() {
        return this.set(Math.ceil(this[0]), Math.ceil(this[1]));
    }

    /**
     * Round both components to the nearest integer
     * @returns {this}
     */
    round() {
        const x = Math.round(this[0]);
        const y = Math.round(this[1]);
        return this.set(x, y);
    }

    /**
     * performs Math.abs operation on x and y
     * @returns {this}
     */
    abs() {
        return this.set(Math.abs(this[0]), Math.abs(this[1]));
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {this}
     */
    _mod(x, y) {
        return this.set(this[0] % x, this[1] % y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    mod(other) {
        return this._mod(other.x, other.y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    divide(other) {
        return this.set(this[0] / other.x, this[1] / other.y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    multiply(other) {
        return this._multiply(other.x, other.y);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {this}
     */
    _multiply(x, y) {
        return this.set(this[0] * x, this[1] * y);
    }

    /**
     * Component-size MAX operator
     * @param {Vector2} other
     * @returns {this}
     */
    max(other) {
        const x = max2(this[0], other.x);
        const y = max2(this[1], other.y);

        return this.set(x, y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {number}
     */
    dot(other) {
        return v2_dot(this[0], this[1], other.x, other.y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    copy(other) {
        return this.set(other.x, other.y);
    }

    /**
     *
     * @returns {Vector2}
     */
    clone() {
        return new Vector2(this[0], this[1]);
    }

    /**
     *
     * @returns {this}
     */
    negate() {
        return this.set(-this[0], -this[1]);
    }

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

    /**
     *
     * @param {Vector2} other
     * @returns {this}
     */
    add(other) {
        return this._add(other.x, other.y);
    }

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

    /**
     *
     * @param {number} val
     * @returns {this}
     */
    addScalar(val) {
        return this._add(val, val);
    }

    /**
     *
     * @param {number} val
     * @returns {this}
     */
    setScalar(val) {
        return this.set(val, val);
    }

    /**
     *
     * @param {number} val
     * @returns {this}
     */
    divideScalar(val) {
        return this.multiplyScalar(1 / val);
    }

    /**
     *
     * @param {number} val
     * @returns {this}
     */
    multiplyScalar(val) {
        assert.isNumber(val, 'val');
        assert.notNaN(val, 'val');

        return this.set(this[0] * val, this[1] * val);
    }

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

    fromJSON(json) {
        if (typeof json === "number") {
            this.set(json, json);
        } else {
            const { x = 0, y = 0 } = json;
            this.set(x, y);
        }
    }

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

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

        this.set(x, y);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    toBinaryBufferFloat32(buffer) {
        buffer.writeFloat32(this[0]);
        buffer.writeFloat32(this[1]);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBufferFloat32(buffer) {
        const x = buffer.readFloat32();
        const y = buffer.readFloat32();

        this.set(x, y);
    }

    /**
     *
     * @returns {boolean}
     */
    isZero() {
        return this[0] === 0 && this[1] === 0;
    }

    /**
     *
     * @param {number} minX
     * @param {number} minY
     * @param {number} maxX
     * @param {number} maxY
     * @returns {this}
     */
    clamp(minX, minY, maxX, maxY) {
        const x = clamp(this[0], minX, maxX);
        const y = clamp(this[1], minY, maxY);
        return this.set(x, y);
    }

    /**
     *
     * @param {number} lowX
     * @param {number} lowY
     * @returns {this}
     */
    clampLow(lowX, lowY) {
        const x = max2(this[0], lowX);
        const y = max2(this[1], lowY);
        return this.set(x, y);
    }

    /**
     *
     * @param {number} highX
     * @param {number} highY
     * @returns {this}
     */
    clampHigh(highX, highY) {
        const x = min2(this[0], highX);
        const y = min2(this[1], highY);
        return this.set(x, y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {number}
     */
    distanceSqrTo(other) {
        return this._distanceSqrTo(other.x, other.y);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {number}
     */
    _distanceSqrTo(x, y) {
        const dx = this[0] - x;
        const dy = this[1] - y;
        return v2_length_sqr(dx, dy);
    }

    /**
     *
     * @param {Vector2} a
     * @param {Vector2} b
     * @param {number} fraction
     * @returns {this}
     */
    lerpVectors(a, b, fraction) {
        const x = lerp(a.x, b.x, fraction);
        const y = lerp(a.y, b.y, fraction);

        return this.set(x, y);
    }

    /**
     *
     * @param {number[]} matrix3
     * @returns {this}
     */
    applyMatrix3(matrix3) {
        const x = this[0];
        const y = this[1];

        const _x = matrix3[0] * x + matrix3[3] * y + matrix3[6];
        const _y = matrix3[1] * x + matrix3[4] * y + matrix3[7];

        return this.set(_x, _y);
    }

    /**
     *
     * @param {Vector2} other
     * @returns {number}
     */
    distanceTo(other) {
        return this._distanceTo(other.x, other.y);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {number}
     */
    _distanceTo(x, y) {
        return Math.sqrt(this._distanceSqrTo(x, y));
    }

    /**
     *
     * @param {Vector2} other
     * @returns {number}
     */
    manhattanDistanceTo(other) {
        const dx = Math.abs(this[0] - other.x);
        const dy = Math.abs(this[1] - other.y);

        return dx + dy;
    }

    /**
     * @returns {number}
     */
    length() {
        return v2_length(this[0], this[1]);
    }

    /**
     * Normalizes the vector, preserving its direction, but making magnitude equal to 1
     * @returns {this}
     */
    normalize() {
        // NOTE: call v2_length directly rather than via `this.length()`.
        // Float64Array exposes `length` as an exotic instance property that V8 inlines
        // through a fast path which bypasses the prototype chain; once an optimized
        // function caller has been specialized for Float64Array shape, `this.length`
        // is read as the array length (a number) rather than our prototype method.
        const l = v2_length(this[0], this[1]);

        if (l === 0) {
            //special case, can't normalize 0 length vector
            return this;
        }

        const m = 1 / l;

        return this.multiplyScalar(m);
    }


    /**
     *
     * @returns {number}
     */
    hash() {
        const x = computeHashFloat(this[0]);
        const y = computeHashFloat(this[1]);

        return ((x << 5) - x) + y;
    }

    /**
     * Rotation is counter-clockwise
     * @param {number} angle in radians
     * @returns {this}
     */
    rotate(angle) {
        const sin = Math.sin(angle);
        const cos = Math.cos(angle);

        const _x = this[0];
        const _y = this[1];

        const x = _x * cos - _y * sin
        const y = _x * sin + _y * cos;

        return this.set(x, y);
    }

    /**
     *
     * @param {function} processor
     * @param {*} [thisArg]
     * @returns {this}
     */
    process(processor, thisArg) {
        processor.call(thisArg, this[0], this[1]);

        this.onChanged.add(processor, thisArg);

        return this;
    }

    toString() {
        return `Vector2{ x:${this[0]}, y:${this[1]} }`;
    }

    /**
     *
     * @param {Vector2} other
     * @returns {boolean}
     */
    equals(other) {
        return this[0] === other.x
            && this[1] === other.y
            ;
    }

    /**
     *
     * @param {Vector2} other
     * @param {number} [tolerance]
     * @return {boolean}
     */
    roughlyEquals(other, tolerance) {
        return this._roughlyEquals(other.x, other.y, tolerance);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} [tolerance] acceptable deviation
     * @return {boolean}
     */
    _roughlyEquals(x, y, tolerance = EPSILON) {
        return epsilonEquals(this[0], x, tolerance)
            && epsilonEquals(this[1], y, tolerance);
    }
}

/**
 * @readonly
 * @type {Vector2}
 */
Vector2.up = new Vector2(0, 1);

/**
 * @readonly
 * @type {Vector2}
 */
Vector2.down = new Vector2(0, -1);

/**
 * @readonly
 * @type {Vector2}
 */
Vector2.left = new Vector2(-1, 0);

/**
 * @readonly
 * @type {Vector2}
 */
Vector2.right = new Vector2(1, 0);

/**
 * @readonly
 * @type {Vector2}
 */
Vector2.zero = new Vector2(0, 0);
/**
 * @readonly
 * @type {Vector2}
 */
Vector2.one = new Vector2(1, 1);

/**
 * @deprecated use {@link Vector2#toArray} instead
 */
Vector2.prototype.writeToArray = Vector2.prototype.toArray;

/**
 * @deprecated use {@link Vector2#fromArray} instead
 */
Vector2.prototype.readFromArray = Vector2.prototype.fromArray;
Vector2._distance = v2_distance;

/**
 * @readonly
 * @type {boolean}
 */
Vector2.prototype.isVector2 = true;
/**
 * @readonly
 * @type {string}
 */
Vector2.typeName = "Vector2";


export default Vector2;
