export interface VectorObject {
    isVectorObject: boolean;
    x: number,
    y: number,
    z: number,
    // Maths
    add: (x: VectorObject | number[] | number, y?: number, z?: number) => VectorObject,
    limit: (max: number) => VectorObject,
    div: (x: VectorObject | number[] | number, y?: number, z?: number) => VectorObject,
    mult: (x: VectorObject | number[] | number, y?: number, z?: number) => VectorObject,
    heading: () => number,
    sub: (x: VectorObject | number[] | number, y?: number, z?: number) => VectorObject,
    setMag: (n: number) => VectorObject,
    magSq: () => number,
    mag: () => number,
    normalize: () => VectorObject,
    copy: () => VectorObject,
    dist: (x: VectorObject | number, y?: number) => number,
    // exports
    toVector2: () => number[]
    toVector3: () => number[]
}
/**
 * Structure and logic are based on p5js.
 */
export const createVector = (x: number, y: number, z: number = 0) => {
    const vectorObject: VectorObject = {
        isVectorObject: true,
        x, y, z,
        /**
         * @param x { VectorObject | Number[] | number }
         * @returns 
         */
        add(x: any, y: number = 0, z: number = 0) {
            if (x instanceof Array) {
                this.x += x[0] || 0;
                this.y += x[1] || 0;
                this.z += x[2] || 0;
                return this;
            }
            if (x.isVectorObject) {
                this.x += x.x || 0;
                this.y += x.y || 0;
                this.z += x.z || 0;
                return this;
            }
            this.x += x || 0;
            this.y += y || 0;
            this.z += z || 0;
            return this;
        },
        limit(max: number) {
            const mSq = this.magSq();
            if (mSq > max * max) {
                this.div(Math.sqrt(mSq)) //normalize it
                    .mult(max);
            }
            return this;
        },
        /**
         * 
         * @param x { VectorObject | Number[] | number }
         * @param y { number }
         * @param z { number }
         * @returns 
         */
        div(x: any, y: number = 0, z: number = 0) {
            if (x.isVectorObject) {
                if (
                    Number.isFinite(x.x) &&
                    Number.isFinite(x.y) &&
                    Number.isFinite(x.z) &&
                    typeof x.x === 'number' &&
                    typeof x.y === 'number' &&
                    typeof x.z === 'number'
                ) {
                    this.x /= x.x;
                    this.y /= x.y;
                    this.z /= x.z;
                }
                return this;
            }
            if (x instanceof Array) {
                if (
                    x.every(element => Number.isFinite(element)) &&
                    x.every(element => typeof element === 'number')
                ) {
                    if (x.some(element => element === 0)) {
                        console.warn('Divide by 0');
                        return this;
                    }

                    if (x.length === 1) {
                        this.x /= x[0];
                        this.y /= x[0];
                        this.z /= x[0];
                    } else if (x.length === 2) {
                        this.x /= x[0];
                        this.y /= x[1];
                    } else if (x.length === 3) {
                        this.x /= x[0];
                        this.y /= x[1];
                        this.z /= x[2];
                    }
                } else {
                    console.warn(
                        'x contains components that are either undefined or not finite numbers'
                    );
                }

                return this;
            }

            const vectorComponents = [...arguments];
            if (
                vectorComponents.every(element => Number.isFinite(element)) &&
                vectorComponents.every(element => typeof element === 'number')
            ) {
                if (vectorComponents.some(element => element === 0)) {
                    console.warn('Divide by 0');
                    return this;
                }

                if (arguments.length === 1) {
                    this.x /= x;
                    this.y /= x;
                    this.z /= x;
                }
                if (arguments.length === 2) {
                    this.x /= x;
                    this.y /= y;
                }
                if (arguments.length === 3) {
                    this.x /= x;
                    this.y /= y;
                    this.z /= z;
                }
            } else {
                console.warn(
                    'x, y, or z arguments are either undefined or not a finite number'
                );
            }

            return this;
        },
        /**
        * 
        * @param x { VectorObject | Number[] | number }
        * @param y { number }
        * @param z { number }
        * @returns 
        */
        mult(x: any, y: number = 0, z: number = 0) {
            if (x.isVectorObject) {
                // new p5.Vector will check that values are valid upon construction but it's possible
                // that someone could change the value of a component after creation, which is why we still
                // perform this check
                if (
                    Number.isFinite(x.x) &&
                    Number.isFinite(x.y) &&
                    Number.isFinite(x.z) &&
                    typeof x.x === 'number' &&
                    typeof x.y === 'number' &&
                    typeof x.z === 'number'
                ) {
                    this.x *= x.x;
                    this.y *= x.y;
                    this.z *= x.z;
                } else {
                    console.warn(
                        'x contains components that are either undefined or not finite numbers'
                    );
                }
                return this;
            }
            if (x instanceof Array) {
                if (
                    x.every(element => Number.isFinite(element)) &&
                    x.every(element => typeof element === 'number')
                ) {
                    if (x.length === 1) {
                        this.x *= x[0];
                        this.y *= x[0];
                        this.z *= x[0];
                    } else if (x.length === 2) {
                        this.x *= x[0];
                        this.y *= x[1];
                    } else if (x.length === 3) {
                        this.x *= x[0];
                        this.y *= x[1];
                        this.z *= x[2];
                    }
                } else {
                    console.warn(
                        'x contains elements that are either undefined or not finite numbers'
                    );
                }
                return this;
            }

            const vectorComponents = [...arguments];
            if (
                vectorComponents.every(element => Number.isFinite(element)) &&
                vectorComponents.every(element => typeof element === 'number')
            ) {
                if (arguments.length === 1) {
                    this.x *= x;
                    this.y *= x;
                    this.z *= x;
                }
                if (arguments.length === 2) {
                    this.x *= x;
                    this.y *= y;
                }
                if (arguments.length === 3) {
                    this.x *= x;
                    this.y *= y;
                    this.z *= z;
                }
            } else {
                console.warn(
                    'x, y, or z arguments are either undefined or not a finite number'
                );
            }

            return this;
        },
        heading() {
            const h = Math.atan2(this.y, this.x);
            return h;
        },
        /*
        * @method sub
        * @param  {VectorObject | Number[]} value the vector to subtract
        * @chainable
        */
        sub(x: any, y: number = 0, z: number = 0) {
            if (x.isVectorObject) {
                this.x -= x.x || 0;
                this.y -= x.y || 0;
                this.z -= x.z || 0;
                return this;
            }
            if (x instanceof Array) {
                this.x -= x[0] || 0;
                this.y -= x[1] || 0;
                this.z -= x[2] || 0;
                return this;
            }
            this.x -= x || 0;
            this.y -= y || 0;
            this.z -= z || 0;
            return this;
        },
        setMag(n) {
            return this.normalize().mult(n);
        },
        magSq() {
            const x = this.x;
            const y = this.y;
            const z = this.z;
            return x * x + y * y + z * z;
        },
        mag() {
            return Math.sqrt(this.magSq());
        },
        normalize() {
            const len = this.mag();
            // here we multiply by the reciprocal instead of calling 'div()'
            // since div duplicates this zero check.
            if (len !== 0) this.mult(1 / len);
            return this;
        },
        copy() {
            return createVector(this.x, this.y, this.z);
        },
        dist(x: any, y: number = 0) {
            if (x.isVectorObject) {
                return Math.hypot(x.x - this.x, x.y - this.y); 
            }
            return Math.hypot(x - this.x, y - this.y); 
        },
        toVector2(){ return [this.x, this.y] },
        toVector3(){ return [this.x, this.y, this.z] },
    }
    return vectorObject;
}

export const FromVector = { create: createVector }
