const DEGRESS_TO_RADIANS = Math.PI/180.0;

const RADIANS_TO_DEGRESS = 180 / Math.PI;

class MathUtils {
    static normalizeDegress(angle){
        if(angle < 0){
            while(angle < -360) angle += 360;

            angle = 360 - angle;
        } else {
            while(angle > 360) angle -= 360;
        }

        return angle;
    }
}

class Quaternion {
    x = 0;
    y = 0;
    z = 0;
    w = 0;
    Quaternion(x=0,y=0,z=0,w=1.0){
        this.x = x;

        this.y = y;

        this.z = z;

        this.w = w;
    }

    fromEuler(euler){
        const x = euler.x;

        const y = euler.y;

        const z = euler.z;
        
        const cos = Math.cos;

        const sin = Math.sin;

		const c1 = cos( x / 2 );

		const c2 = cos( y / 2 );

		const c3 = cos( z / 2 );

		const s1 = sin( x / 2 );

		const s2 = sin( y / 2 );

		const s3 = sin( z / 2 );

        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;
    }
}

class Vector2 {
    x = 0.0;
    y = 0.0;
    constructor(x = 0.0, y = 0.0){
        this.x = x;

        this.y = y;
    }

    /**
     *@param {number} angle
     *@param {number} range
     */
    toAngle(range, angle){
        this.x = range;

        this.y = 0;

        this.rotate(angle);

        return this;
    }

    set(x = this.x, y = this.y){
        this.x = x;

        this.y = y;

        return this;
    }

    add(x=0,y=0) {
        this.x += x;
        this.y += y;
        return this;
    }

    distance(x, y){
        return  Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2));
    }

    getDegress(){
        return Math.atan2(this.y, this.x) * RADIANS_TO_DEGRESS;
    }

    rotateRad(radians){
        let cos = Math.cos(radians);

        let sin = Math.sin(radians);
        
        let x = this.x * cos + this.y * sin;

        let y = this.x * sin - this.y * cos;

        this.x = x;

        this.y = y;

        return this;
    }

    multiplyScalar(scale) {
        this.x *= scale;

        this.y *= scale;

        this.z *= scale;

        return this;
    }
    
    floor() {
        this.x = Math.floor(this.x);

        this.y = Math.floor(this.y);

        return this;
    }

    normalize() {
        return this.divideScalar(this.length() || 1 );
    }

    divideScalar(scale) {
        return this.multiplyScalar(1.0 / scale);
    }

    length() {
        return Math.sqrt((this.x * this.x) * (this.y * this.y));
    }

    round() {
        this.x = Math.round(this.x);

        this.y = Math.round(this.y);

        return this;
    }

    /** @param {Vector3} vec2  */
    multiply(vec2) {
        this.x *= vec2.x;

        this.y *= vec2.y;

        return this;
    }

    /** @param {Vector3} vec2  */
    divide(vec2) {
        this.x /= vec2.x;

        this.y /= vec2.y;

        return this;
    }

    /**
     * @param {Vector2} pivot
     * @param {number} angle  
     * */
    rotate(angle, pivot=ZERO_VEC2){
        this.x -= pivot.x;

        this.y -= pivot.y;

        this.rotateRad(angle * DEGRESS_TO_RADIANS );

        this.x += pivot.x;

        this.y += pivot.y;

        return this;
    }
}

class Vector3 {
    x = 0.0;
    y = 0.0;
    z = 0.0;
    _q = new Quaternion();

    constructor(x=0.0, y=0.0, z=0.0){
        if(typeof x == "object") {
            this.y = x.y ?? 0;

            this.z = x.z ?? 0;

            this.x = x.x ?? 0;
        } else {
            this.x = x;

            this.y = y;

            this.z = z;
        }
    }

    add(vec){
        this.x += vec.x;

        this.y += vec.y;

        this.z += vec.z;

        return this;
    }

    sub(vec){
        this.x -= vec.x;

        this.y -= vec.y;

        this.z -= vec.z;

        return this;
    }
    distance(VectorB, is2d){
        const dx = this.x - VectorB.x

        const dy = this.y - VectorB.y

        const dz = this.z - VectorB.z

        if(is2d){
            return Math.sqrt(Math.pow(dx, 2) + Math.pow(dz, 2));
        }

        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2) + Math.pow(dz, 2));
    }
    distanceEuclidian(VectorB, is2d){
        const dx = this.x - VectorB.x

        const dy = this.y - VectorB.y

        const dz = this.z - VectorB.z

        if(is2d){
            return (dx * dx) + (dz * dz);
        }

        return (dx * dx) + (dy * dy) + (dz * dz);
    }
    set(x,y,z){
        this.x = x;

        this.y = y;

        this.z = z;

        return this;
    }

    multiplyScalar(scale) {
        this.x *= scale;

        this.y *= scale;

        this.z *= scale;

        return this;
    }

    round() {
        this.x = Math.round(this.x);

        this.y = Math.round(this.y);

        this.z = Math.round(this.z);

        return this;
    }
    
    floor() {
        this.x = Math.floor(this.x);

        this.y = Math.floor(this.y);

        this.z = Math.floor(this.z);

        return this;
    }

    /** @param {Vector3} vec3  */
    multiply(vec3) {
        this.x *= vec3.x;

        this.y *= vec3.y;

        this.z *= vec3.z;

        return this;
    }

    /** @param {Vector3} vec3  */
    divide(vec3) {
        this.x /= vec3.x;

        this.y /= vec3.y;

        this.z /= vec3.z;

        return this;
    }

    normalize() {
        return this.divideScalar(this.length() || 1 );
    }

    divideScalar(scale) {
        return this.multiplyScalar(1.0 / scale);
    }

    length() {
        return Math.sqrt((this.x * this.x) * (this.y * this.y) + (this.z * this.z));
    }

    /**
     * @param {Vector2} angles 
     * @param {number} range  
     */
    toAngle(range, angles){
        let rotator = new Vector2();

        rotator.toAngle(-range, angles.y);

        this.x = rotator.x;

        this.z = rotator.y;

        rotator.toAngle(-range, angles.x);

        this.y = rotator.y;

        return this;
    }

    /**@param {Vector3} euler  */
    rotateByEuler(euler){
        this._q.fromEuler(euler);

        const x = this.x, y = this.y, z = this.z;

        const qx = this._q.x, qy = this._q.y, qz = this._q.z, qw = this._q.w;

        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 * qz * z;

		this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy;

		this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz;

		this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx;

        return this;
    }

    /**@param {Vector2} axisAngle  */
    applyDegress2D(axisAngle, pivot=ZERO_VEC3){
        this.x -= pivot.x;

        this.y -= pivot.y;

        this.z -= pivot.z;

        this.rotateByEuler(new Vector3(axisAngle.x * DEGRESS_TO_RADIANS,axisAngle.y * DEGRESS_TO_RADIANS,0));

        this.x += pivot.x;

        this.y += pivot.y;

        this.z += pivot.z;

        return this;
    }

    clone(){
        return new Vector3(this.x, this.y, this.z);
    }
}

const ZERO_VEC2 = new Vector2(0,0);

const ZERO_VEC3 = new Vector3(0,0);

export { Vector2, Vector3, MathUtils, Quaternion }
