    import { Quaternion } from "./Quaternion";
    import { degToRad } from "./utils/degToRad";
    import { clamp } from "./utils/clamp";
    import { Sphere } from "../shapes";

    var mRotation: Matrix4;
    var mScale: Matrix4;
    var x: Vector3;
    var y: Vector3;
    var z: Vector3;

    /** @class
     *  @subcategory  Math
     * */
    export class Matrix4 {
      elements: Float32Array;
      constructor(n11: Array<number>);
      constructor(
        n11?: number,
        n12?: number,
        n13?: number,
        n14?: number,
        n21?: number,
        n22?: number,
        n23?: number,
        n24?: number,
        n31?: number,
        n32?: number,
        n33?: number,
        n34?: number,
        n41?: number,
        n42?: number,
        n43?: number,
        n44?: number
      );
      constructor(
        n11: Array<number> | number = 1,
        n12: number = 0,
        n13: number = 0,
        n14: number = 0,
        n21: number = 0,
        n22: number = 1,
        n23: number = 0,
        n24: number = 0,
        n31: number = 0,
        n32: number = 0,
        n33: number = 1,
        n34: number = 0,
        n41: number = 0,
        n42: number = 0,
        n43: number = 0,
        n44: number = 1
      ) {
        if (typeof n11 !== "undefined" && typeof n11 !== "number") {
          // passing list like initialization
          this.elements = new Float32Array(n11);
        } else {
          this.elements = new Float32Array(16);
          this.elements[0] = n11;
          this.elements[4] = n12;
          this.elements[8] = n13;
          this.elements[12] = n14;
          this.elements[1] = n21;
          this.elements[5] = n22;
          this.elements[9] = n23;
          this.elements[13] = n24;
          this.elements[2] = n31;
          this.elements[6] = n32;
          this.elements[10] = n33;
          this.elements[14] = n34;
          this.elements[3] = n41;
          this.elements[7] = n42;
          this.elements[11] = n43;
          this.elements[15] = n44;
        }
      }

      // eslint-disable-next-line no-unused-vars, class-methods-use-this
      makeScale(x: any, y: any, z: any) {
        throw new Error("Method not implemented.");
      }

      set(
        n11: number,
        n12: number,
        n13: number,
        n14: number,
        n21: number,
        n22: number,
        n23: number,
        n24: number,
        n31: number,
        n32: number,
        n33: number,
        n34: number,
        n41: number,
        n42: number,
        n43: number,
        n44: number
      ) {
        const te = this.elements;

        te[0] = n11;
        te[4] = n12;
        te[8] = n13;
        te[12] = n14;
        te[1] = n21;
        te[5] = n22;
        te[9] = n23;
        te[13] = n24;
        te[2] = n31;
        te[6] = n32;
        te[10] = n33;
        te[14] = n34;
        te[3] = n41;
        te[7] = n42;
        te[11] = n43;
        te[15] = n44;

        return this;
      }

      identity() {
        this.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);

        return this;
      }

      copy(m: { elements: any }) {
        const me = m.elements;

        this.set(
          me[0],
          me[4],
          me[8],
          me[12],
          me[1],
          me[5],
          me[9],
          me[13],
          me[2],
          me[6],
          me[10],
          me[14],
          me[3],
          me[7],
          me[11],
          me[15]
        );

        return this;
      }

      matrix3FromTopLeft() {
        const te = this.elements;
        return new Matrix3(
          te[0],
          te[4],
          te[8],
          te[1],
          te[5],
          te[9],
          te[2],
          te[6],
          te[10]
        );
      }

      setRotationFromEuler(v: Vector3, order?: string) {
        const te = this.elements;

        const { x, y, z } = v;
        const a = Math.cos(x);
        const b = Math.sin(x);
        const c = Math.cos(y);
        const d = Math.sin(y);
        const e = Math.cos(z);
        const f = Math.sin(z);

        if (order === undefined || order === "XYZ") {
          const ae = a * e;
          const af = a * f;
          const be = b * e;
          const bf = b * f;

          te[0] = c * e;
          te[4] = -c * f;
          te[8] = d;

          te[1] = af + be * d;
          te[5] = ae - bf * d;
          te[9] = -b * c;

          te[2] = bf - ae * d;
          te[6] = be + af * d;
          te[10] = a * c;
        } else
          console.error(`Error with matrix4 setRotationFromEuler. Order: ${order}`);

        return this;
      }

      setRotationFromQuaternion(q: Quaternion) {
        const te = this.elements;

        const { x, y, z, w } = q;
        const x2 = x + x;
        const y2 = y + y;
        const z2 = z + z;
        const xx = x * x2;
        const xy = x * y2;
        const xz = x * z2;
        const yy = y * y2;
        const yz = y * z2;
        const zz = z * z2;
        const wx = w * x2;
        const wy = w * y2;
        const wz = w * z2;

        te[0] = 1 - (yy + zz);
        te[4] = xy - wz;
        te[8] = xz + wy;

        te[1] = xy + wz;
        te[5] = 1 - (xx + zz);
        te[9] = yz - wx;

        te[2] = xz - wy;
        te[6] = yz + wx;
        te[10] = 1 - (xx + yy);

        return this;
      }

      multiplyMatrices(a: { elements: any }, b: Matrix4) {
        const ae = a.elements;
        const be = b.elements;
        const te = this.elements;

        const a11 = ae[0];
        const a12 = ae[4];
        const a13 = ae[8];
        const a14 = ae[12];
        const a21 = ae[1];
        const a22 = ae[5];
        const a23 = ae[9];
        const a24 = ae[13];
        const a31 = ae[2];
        const a32 = ae[6];
        const a33 = ae[10];
        const a34 = ae[14];
        const a41 = ae[3];
        const a42 = ae[7];
        const a43 = ae[11];
        const a44 = ae[15];

        const b11 = be[0];
        const b12 = be[4];
        const b13 = be[8];
        const b14 = be[12];
        const b21 = be[1];
        const b22 = be[5];
        const b23 = be[9];
        const b24 = be[13];
        const b31 = be[2];
        const b32 = be[6];
        const b33 = be[10];
        const b34 = be[14];
        const b41 = be[3];
        const b42 = be[7];
        const b43 = be[11];
        const b44 = be[15];

        te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
        te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
        te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
        te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;

        te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
        te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
        te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
        te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;

        te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
        te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
        te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
        te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;

        te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
        te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
        te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
        te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;

        return this;
      }

      multiplyScalar(s: number) {
        const te = this.elements;

        te[0] *= s;
        te[4] *= s;
        te[8] *= s;
        te[12] *= s;
        te[1] *= s;
        te[5] *= s;
        te[9] *= s;
        te[13] *= s;
        te[2] *= s;
        te[6] *= s;
        te[10] *= s;
        te[14] *= s;
        te[3] *= s;
        te[7] *= s;
        te[11] *= s;
        te[15] *= s;

        return this;
      }

      makeTranslation(x: any, y: any, z: any) {
        this.set(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1);

        return this;
      }

      // snap values close to integers to their integer value
      // useful and identifying identity matrices
      snap(digits: number) {
        if (!digits) digits = 4;
        const mult = 10 ** 4;
        const te = this.elements;
        for (let i = 0; i < 16; i++) {
          const rounded = Math.round(te[i]);
          if (rounded === Math.round(te[i] * mult) / mult) {
            te[i] = rounded;
          }
        }
        return this;
      }

      transpose() {
        const te = this.elements;
        let tmp: any;

        tmp = te[1];
        te[1] = te[4];
        te[4] = tmp;
        tmp = te[2];
        te[2] = te[8];
        te[8] = tmp;
        tmp = te[6];
        te[6] = te[9];
        te[9] = tmp;

        tmp = te[3];
        te[3] = te[12];
        te[12] = tmp;
        tmp = te[7];
        te[7] = te[13];
        te[13] = tmp;
        tmp = te[11];
        te[11] = te[14];
        te[14] = tmp;

        return this;
      }

      setPosition(v: Vector3) {
        const te = this.elements;

        te[12] = v.x;
        te[13] = v.y;
        te[14] = v.z;

        return this;
      }

      translate(v: Vector3) {
        const te = this.elements;

        te[12] += v.x;
        te[13] += v.y;
        te[14] += v.z;

        return this;
      }

      getInverse(m: Matrix4, throwOnInvertible?: boolean) {
        // based on
        // http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
        const te = this.elements;
        const me = m.elements;

        const n11 = me[0];
        const n12 = me[4];
        const n13 = me[8];
        const n14 = me[12];
        const n21 = me[1];
        const n22 = me[5];
        const n23 = me[9];
        const n24 = me[13];
        const n31 = me[2];
        const n32 = me[6];
        const n33 = me[10];
        const n34 = me[14];
        const n41 = me[3];
        const n42 = me[7];
        const n43 = me[11];
        const n44 = me[15];

        te[0] =
          n23 * n34 * n42 -
          n24 * n33 * n42 +
          n24 * n32 * n43 -
          n22 * n34 * n43 -
          n23 * n32 * n44 +
          n22 * n33 * n44;
        te[4] =
          n14 * n33 * n42 -
          n13 * n34 * n42 -
          n14 * n32 * n43 +
          n12 * n34 * n43 +
          n13 * n32 * n44 -
          n12 * n33 * n44;
        te[8] =
          n13 * n24 * n42 -
          n14 * n23 * n42 +
          n14 * n22 * n43 -
          n12 * n24 * n43 -
          n13 * n22 * n44 +
          n12 * n23 * n44;
        te[12] =
          n14 * n23 * n32 -
          n13 * n24 * n32 -
          n14 * n22 * n33 +
          n12 * n24 * n33 +
          n13 * n22 * n34 -
          n12 * n23 * n34;
        te[1] =
          n24 * n33 * n41 -
          n23 * n34 * n41 -
          n24 * n31 * n43 +
          n21 * n34 * n43 +
          n23 * n31 * n44 -
          n21 * n33 * n44;
        te[5] =
          n13 * n34 * n41 -
          n14 * n33 * n41 +
          n14 * n31 * n43 -
          n11 * n34 * n43 -
          n13 * n31 * n44 +
          n11 * n33 * n44;
        te[9] =
          n14 * n23 * n41 -
          n13 * n24 * n41 -
          n14 * n21 * n43 +
          n11 * n24 * n43 +
          n13 * n21 * n44 -
          n11 * n23 * n44;
        te[13] =
          n13 * n24 * n31 -
          n14 * n23 * n31 +
          n14 * n21 * n33 -
          n11 * n24 * n33 -
          n13 * n21 * n34 +
          n11 * n23 * n34;
        te[2] =
          n22 * n34 * n41 -
          n24 * n32 * n41 +
          n24 * n31 * n42 -
          n21 * n34 * n42 -
          n22 * n31 * n44 +
          n21 * n32 * n44;
        te[6] =
          n14 * n32 * n41 -
          n12 * n34 * n41 -
          n14 * n31 * n42 +
          n11 * n34 * n42 +
          n12 * n31 * n44 -
          n11 * n32 * n44;
        te[10] =
          n12 * n24 * n41 -
          n14 * n22 * n41 +
          n14 * n21 * n42 -
          n11 * n24 * n42 -
          n12 * n21 * n44 +
          n11 * n22 * n44;
        te[14] =
          n14 * n22 * n31 -
          n12 * n24 * n31 -
          n14 * n21 * n32 +
          n11 * n24 * n32 +
          n12 * n21 * n34 -
          n11 * n22 * n34;
        te[3] =
          n23 * n32 * n41 -
          n22 * n33 * n41 -
          n23 * n31 * n42 +
          n21 * n33 * n42 +
          n22 * n31 * n43 -
          n21 * n32 * n43;
        te[7] =
          n12 * n33 * n41 -
          n13 * n32 * n41 +
          n13 * n31 * n42 -
          n11 * n33 * n42 -
          n12 * n31 * n43 +
          n11 * n32 * n43;
        te[11] =
          n13 * n22 * n41 -
          n12 * n23 * n41 -
          n13 * n21 * n42 +
          n11 * n23 * n42 +
          n12 * n21 * n43 -
          n11 * n22 * n43;
        te[15] =
          n12 * n23 * n31 -
          n13 * n22 * n31 +
          n13 * n21 * n32 -
          n11 * n23 * n32 -
          n12 * n21 * n33 +
          n11 * n22 * n33;

        const det = n11 * te[0] + n21 * te[4] + n31 * te[8] + n41 * te[12];

        if (det === 0) {
          const msg = "Matrix4.getInverse(): can't invert matrix, determinant is 0";

          if (throwOnInvertible || false) {
            throw new Error(msg);
          } else {
            console.warn(msg);
          }

          this.identity();

          return this;
        }

        this.multiplyScalar(1 / det);

        return this;
      }

      isReflected() {
        const te = this.elements;

        const m0 = te[0];
        const m3 = te[4];
        const m6 = te[8];
        const m1 = te[1];
        const m4 = te[5];
        const m7 = te[9];
        const m2 = te[2];
        const m5 = te[6];
        const m8 = te[10];

        const determinant =
          m0 * m4 * m8 + // +aei
          m1 * m5 * m6 + // +bfg
          m2 * m3 * m7 - // +cdh
          m2 * m4 * m6 - // -ceg
          m1 * m3 * m8 - // -bdi
          m0 * m5 * m7; // -afh

        return determinant < 0;
      }

      scale(v: { x?: any; y?: any; z?: any }) {
        const te = this.elements;
        const { x } = v;
        const { y } = v;
        const { z } = v;

        te[0] *= x;
        te[4] *= y;
        te[8] *= z;
        te[1] *= x;
        te[5] *= y;
        te[9] *= z;
        te[2] *= x;
        te[6] *= y;
        te[10] *= z;
        te[3] *= x;
        te[7] *= y;
        te[11] *= z;

        return this;
      }

      getMaxScaleOnAxis() {
        const te = this.elements;

        const scaleXSq = te[0] * te[0] + te[1] * te[1] + te[2] * te[2];
        const scaleYSq = te[4] * te[4] + te[5] * te[5] + te[6] * te[6];
        const scaleZSq = te[8] * te[8] + te[9] * te[9] + te[10] * te[10];

        return Math.sqrt(Math.max(scaleXSq, Math.max(scaleYSq, scaleZSq)));
      }

      makeFrustum(
        left: number,
        right: number,
        bottom: number,
        top: number,
        near: number,
        far: number
      ) {
        const te = this.elements;

        const x = (2 * near) / (right - left);
        const y = (2 * near) / (top - bottom);

        const a = (right + left) / (right - left);
        const b = (top + bottom) / (top - bottom);
        const c = -(far + near) / (far - near);
        const d = (-2 * far * near) / (far - near);

        te[0] = x;
        te[4] = 0;
        te[8] = a;
        te[12] = 0;
        te[1] = 0;
        te[5] = y;
        te[9] = b;
        te[13] = 0;
        te[2] = 0;
        te[6] = 0;
        te[10] = c;
        te[14] = d;
        te[3] = 0;
        te[7] = 0;
        te[11] = -1;
        te[15] = 0;

        return this;
      }

      makePerspective(fov: number, aspect: number, near: number, far: any) {
        const ymax = near * Math.tan(degToRad(fov * 0.5));
        const ymin = -ymax;
        const xmin = ymin * aspect;
        const xmax = ymax * aspect;

        return this.makeFrustum(xmin, xmax, ymin, ymax, near, far);
      }

      makeOrthographic(
        left: number,
        right: number,
        top: number,
        bottom: number,
        near: number,
        far: number
      ) {
        const te = this.elements;
        const w = 1.0 / (right - left);
        const h = 1.0 / (top - bottom);
        const p = 1.0 / (far - near);

        const x = (right + left) * w;
        const y = (top + bottom) * h;
        const z = (far + near) * p;

        te[0] = 2 * w;
        te[4] = 0;
        te[8] = 0;
        te[12] = -x;
        te[1] = 0;
        te[5] = 2 * h;
        te[9] = 0;
        te[13] = -y;
        te[2] = 0;
        te[6] = 0;
        te[10] = -2 * p;
        te[14] = -z;
        te[3] = 0;
        te[7] = 0;
        te[11] = 0;
        te[15] = 1;

        return this;
      }

      isEqual(m: { elements: any }) {
        const me = m.elements;
        const te = this.elements;

        if (
          te[0] === me[0] &&
          te[4] === me[4] &&
          te[8] === me[8] &&
          te[12] === me[12] &&
          te[1] === me[1] &&
          te[5] === me[5] &&
          te[9] === me[9] &&
          te[13] === me[13] &&
          te[2] === me[2] &&
          te[6] === me[6] &&
          te[10] === me[10] &&
          te[14] === me[14] &&
          te[3] === me[3] &&
          te[7] === me[7] &&
          te[11] === me[11] &&
          te[15] === me[15]
        ) {
          return true;
        }
        return false;
      }

      clone() {
        const te = this.elements;

        return new Matrix4(
          te[0],
          te[4],
          te[8],
          te[12],
          te[1],
          te[5],
          te[9],
          te[13],
          te[2],
          te[6],
          te[10],
          te[14],
          te[3],
          te[7],
          te[11],
          te[15]
        );
      }

      isIdentity() {
        const te = this.elements;

        if (
          te[0] === 1 &&
          te[4] === 0 &&
          te[8] === 0 &&
          te[12] === 0 &&
          te[1] === 0 &&
          te[5] === 1 &&
          te[9] === 0 &&
          te[13] === 0 &&
          te[2] === 0 &&
          te[6] === 0 &&
          te[10] === 1 &&
          te[14] === 0 &&
          te[3] === 0 &&
          te[7] === 0 &&
          te[11] === 0 &&
          te[15] === 1
        ) {
          return true;
        }
        return false;
      }

      // return true if elements are with digits of identity
      isNearlyIdentity(digits: any) {
        const snapped = this.clone().snap(digits);
        return snapped.isIdentity();
      }

      getScale(scale?: Vector3): Vector3 {
        const te = this.elements;
        scale = scale || new Vector3();
        // grab the axis vectors
        x.set(te[0], te[1], te[2]);
        y.set(te[4], te[5], te[6]);
        z.set(te[8], te[9], te[10]);

        scale.x = x.length();
        scale.y = y.length();
        scale.z = z.length();

        return scale;
      }

      lookAt(eye: Vector3, target: Vector3, up: Vector3) {
        const te = this.elements;

        z.subVectors(eye, target).normalize();

        if (z.length() === 0) {
          z.z = 1;
        }

        x.crossVectors(up, z).normalize();

        if (x.length() === 0) {
          z.x += 0.0001;
          x.crossVectors(up, z).normalize();
        }

        y.crossVectors(z, x);

        te[0] = x.x;
        te[4] = y.x;
        te[8] = z.x;
        te[1] = x.y;
        te[5] = y.y;
        te[9] = z.y;
        te[2] = x.z;
        te[6] = y.z;
        te[10] = z.z;

        return this;
      }

      compose(translation: Vector3, rotation: Quaternion, scale: Vector3) {
        const te = this.elements;

        mRotation.identity();
        mRotation.setRotationFromQuaternion(rotation);

        mScale.makeScale(scale.x, scale.y, scale.z);

        this.multiplyMatrices(mRotation, mScale);

        te[12] = translation.x;
        te[13] = translation.y;
        te[14] = translation.z;

        return this;
      }
    }

    mRotation = new Matrix4();
    mScale = new Matrix4();

    /**
     * @interface
     */
    export interface XYZ {
      /**  */
      x: number;
      /**  */
      y: number;
      /**  */
      z: number;
    }

    /** @class
     *  @subcategory  Math
     * */
    export class Vector3 {
      // unaccounted for assignents to vector3 properties in other parts of the code
      // look in glcartoon.js for example
      color?: any;
      resi?: any;
      style?: any;
      smoothen?: any;
      atom?: any;
      skip?: any;
      atomid?: undefined;

      x: number;
      y: number;
      z: number;

      constructor(x?: number, y?: number, z?: number) {
        this.x = x || 0.0;
        this.y = y || 0.0;
        this.z = z || 0.0;
        this.atomid = undefined;
      }

      set(x: any, y: any, z: any) {
        this.x = x;
        this.y = y;
        this.z = z;

        return this;
      }

      copy(v: { x: any; y: any; z: any }) {
        this.x = v.x;
        this.y = v.y;
        this.z = v.z;

        return this;
      }

      add(v: { x: any; y: any; z: any }) {
        this.x += v.x;
        this.y += v.y;
        this.z += v.z;

        return this;
      }

      addVectors(a: { x: any; y: any; z: any }, b: { x: any; y: any; z: any }) {
        this.x = a.x + b.x;
        this.y = a.y + b.y;
        this.z = a.z + b.z;

        return this;
      }

      multiplyVectors(
        a: { x: number; y: number; z: number },
        b: { x: number; y: number; z: number }
      ) {
        // elementwise
        this.x = a.x * b.x;
        this.y = a.y * b.y;
        this.z = a.z * b.z;

        return this;
      }

      sub(v: { x: number; y: number; z: number }) {
        this.x -= v.x;
        this.y -= v.y;
        this.z -= v.z;

        return this;
      }

      subVectors(
        a: { x: number; y: number; z: number },
        b: { x: number; y: number; z: number }
      ) {
        this.x = a.x - b.x;
        this.y = a.y - b.y;
        this.z = a.z - b.z;

        return this;
      }

      multiplyScalar(s: number) {
        this.x *= s;
        this.y *= s;
        this.z *= s;

        return this;
      }

      divideScalar(s: number) {
        if (s !== 0) {
          this.x /= s;
          this.y /= s;
          this.z /= s;
        } else {
          this.x = 0;
          this.y = 0;
          this.z = 0;
        }

        return this;
      }

      // accumulate maximum
      max(s: { x: number; y: number; z: number }) {
        this.x = Math.max(this.x, s.x);
        this.y = Math.max(this.y, s.y);
        this.z = Math.max(this.z, s.z);

        return this;
      }

      // accumulate min
      min(s: { x: number; y: number; z: number }) {
        this.x = Math.min(this.x, s.x);
        this.y = Math.min(this.y, s.y);
        this.z = Math.min(this.z, s.z);

        return this;
      }

      distanceTo(v: any) {
        return Math.sqrt(this.distanceToSquared(v));
      }

      distanceToSquared(v: { x: number; y: number; z: number }) {
        const dx = this.x - v.x;
        const dy = this.y - v.y;
        const dz = this.z - v.z;

        return dx * dx + dy * dy + dz * dz;
      }

      applyMatrix3(m: { elements: any }) {
        const { x } = this;
        const { y } = this;
        const { z } = this;

        const e = m.elements;
        // column major ordering
        this.x = e[0] * x + e[3] * y + e[6] * z;
        this.y = e[1] * x + e[4] * y + e[7] * z;
        this.z = e[2] * x + e[5] * y + e[8] * z;

        return this;
      }

      applyMatrix4(m: { elements: any }) {
        const { x } = this;
        const { y } = this;
        const { z } = this;

        const e = m.elements;

        this.x = e[0] * x + e[4] * y + e[8] * z + e[12];
        this.y = e[1] * x + e[5] * y + e[9] * z + e[13];
        this.z = e[2] * x + e[6] * y + e[10] * z + e[14];

        return this;
      }

      applyProjection(m: { elements: any }) {
        // input: Matrix4 projection matrix
        const { x } = this;
        const { y } = this;
        const { z } = this;

        const e = m.elements;
        const d = e[3] * x + e[7] * y + e[11] * z + e[15];

        this.x = (e[0] * x + e[4] * y + e[8] * z + e[12]) / d;
        this.y = (e[1] * x + e[5] * y + e[9] * z + e[13]) / d;
        this.z = (e[2] * x + e[6] * y + e[10] * z + e[14]) / d;

        return this;
      }

      applyQuaternion(q: Quaternion): Vector3 {
        // save values
        const { x } = this;
        const { y } = this;
        const { z } = this;

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

        // compute this as
        // t = 2 * cross(q.xyz, v)
        // newv = v + q.w * t + cross(q.xyz, t)
        // this from molecularmusings
        // http://molecularmusings.wordpress.com/2013/05/24/a-faster-quaternion-vector-multiplication/
        const t: { x?: number; y?: number; z?: number } = {};
        t.x = 2 * (y * qz - z * qy);
        t.y = 2 * (z * qx - x * qz);
        t.z = 2 * (x * qy - y * qx);

        // cross t with q
        const t2: { x?: number; y?: number; z?: number } = {};
        t2.x = t.y * qz - t.z * qy;
        t2.y = t.z * qx - t.x * qz;
        t2.z = t.x * qy - t.y * qx;

        this.x = x + qw * t.x + t2.x;
        this.y = y + qw * t.y + t2.y;
        this.z = z + qw * t.z + t2.z;

        return this;
      }

      negate() {
        return this.multiplyScalar(-1);
      }

      dot(v: Vector3) {
        return this.x * v.x + this.y * v.y + this.z * v.z;
      }

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

      lengthSq() {
        return this.x * this.x + this.y * this.y + this.z * this.z;
      }

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

      cross(v: Vector3) {
        const { x } = this;
        const { y } = this;
        const { z } = this;

        this.x = y * v.z - z * v.y;
        this.y = z * v.x - x * v.z;
        this.z = x * v.y - y * v.x;

        return this;
      }

      crossVectors(a: Vector3, b: Vector3) {
        this.x = a.y * b.z - a.z * b.y;
        this.y = a.z * b.x - a.x * b.z;
        this.z = a.x * b.y - a.y * b.x;

        return this;
      }

      equals(b: Vector3) {
        return this.x == b.x && this.y == b.y && this.z == b.z;
      }

      getPositionFromMatrix(m: Matrix4) {
        this.x = m.elements[12];
        this.y = m.elements[13];
        this.z = m.elements[14];

        return this;
      }

      setEulerFromRotationMatrix(m: Matrix4, order: string) {
        // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
        const te = m.elements;
        const m11 = te[0];
        const m12 = te[4];
        const m13 = te[8];
        // var m21 = te[1];
        const m22 = te[5];
        const m23 = te[9];
        // var m31 = te[2];
        const m32 = te[6];
        const m33 = te[10];

        if (order === undefined || order === "XYZ") {
          this.y = Math.asin(clamp(m13, -1, 1));

          if (Math.abs(m13) < 0.99999) {
            this.x = Math.atan2(-m23, m33);
            this.z = Math.atan2(-m12, m11);
          } else {
            this.x = Math.atan2(m32, m22);
            this.z = 0;
          }
        } else {
          console.error(
            `Error with vector's setEulerFromRotationMatrix: Unknown order: ${order}`
          );
        }

        return this;
      }

      rotateAboutVector(axis: Vector3, ang: number) {
        axis.normalize();
        const cosang = Math.cos(ang);
        const sinang = Math.sin(ang);
        // Rodrigues' rotation formula, from wikipedia
        const term1 = this.clone().multiplyScalar(cosang);
        const term2 = axis.clone().cross(this).multiplyScalar(sinang);
        const term3 = axis
          .clone()
          .multiplyScalar(axis.clone().dot(this))
          .multiplyScalar(1 - cosang);

        const rot = term1.add(term2).add(term3);

        this.x = rot.x;
        this.y = rot.y;
        this.z = rot.z;

        return this;
      }

      setFromMatrixPosition(m: { elements: any }) {
        const e = m.elements;

        this.x = e[12];
        this.y = e[13];
        this.z = e[14];

        return this;
      }

      // unproject is defined after Matrix4
      transformDirection(m: { elements: any }) {
        // input: THREE.Matrix4 affine matrix
        // vector interpreted as a direction
        const { x } = this;
        const { y } = this;
        const { z } = this;
        const e = m.elements;

        this.x = e[0] * x + e[4] * y + e[8] * z;
        this.y = e[1] * x + e[5] * y + e[9] * z;
        this.z = e[2] * x + e[6] * y + e[10] * z;

        return this.normalize();
      }

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

      unproject(camera: { matrixWorld: any; projectionMatrix: any }) {
        const mat4 = mRotation;
        mat4.multiplyMatrices(
          camera.matrixWorld,
          mat4.getInverse(camera.projectionMatrix)
        );
        return this.applyMatrix4(mat4);
      }
    }

    x = new Vector3();
    y = new Vector3();
    z = new Vector3();

    /*
     * @interface Matrix3
     */
    export interface Matrix3 {
      elements: Float32Array;
    }

    /** @class
     *  @subcategory  Math
     * */
    export class Matrix3 {
      constructor(
        n11: number = 1,
        n12: number = 0,
        n13: number = 0,
        n21: number = 0,
        n22: number = 1,
        n23: number = 0,
        n31: number = 0,
        n32: number = 0,
        n33: number = 1
      ) {
        this.elements = new Float32Array(9);
        this.set(n11, n12, n13, n21, n22, n23, n31, n32, n33);
      }

      set(
        n11: number,
        n12: number,
        n13: number,
        n21: number,
        n22: number,
        n23: number,
        n31: number,
        n32: number,
        n33: number
      ) {
        const te = this.elements;

        te[0] = n11;
        te[3] = n12;
        te[6] = n13;
        te[1] = n21;
        te[4] = n22;
        te[7] = n23;
        te[2] = n31;
        te[5] = n32;
        te[8] = n33;

        return this;
      }

      identity() {
        this.set(1, 0, 0, 0, 1, 0, 0, 0, 1);

        return this;
      }

      copy(m: { elements: any }) {
        const me = m.elements;

        this.set(me[0], me[3], me[6], me[1], me[4], me[7], me[2], me[5], me[8]);
      }

      multiplyScalar(s: number) {
        const te = this.elements;

        te[0] *= s;
        te[3] *= s;
        te[6] *= s;
        te[1] *= s;
        te[4] *= s;
        te[7] *= s;
        te[2] *= s;
        te[5] *= s;
        te[8] *= s;

        return this;
      }

      getInverse3(matrix: { elements: any }) {
        // input: Matrix3
        const me = matrix.elements;
        const te = this.elements;

        te[0] = me[4] * me[8] - me[5] * me[7];
        te[3] = me[6] * me[5] - me[3] * me[8];
        te[6] = me[3] * me[7] - me[6] * me[4];
        te[1] = me[7] * me[2] - me[1] * me[8];
        te[4] = me[0] * me[8] - me[6] * me[2];
        te[7] = me[1] * me[6] - me[0] * me[7];
        te[2] = me[1] * me[5] - me[2] * me[4];
        te[5] = me[2] * me[3] - me[0] * me[5];
        te[8] = me[0] * me[4] - me[1] * me[3];

        const det = me[0] * te[0] + me[3] * te[1] + me[6] * te[2];
        this.multiplyScalar(1.0 / det);

        return this;
      }

      getInverse(matrix: { elements: any }, throwOnInvertible: any) {
        // input: Matrix4
        const me = matrix.elements;
        const te = this.elements;

        te[0] = me[10] * me[5] - me[6] * me[9];
        te[1] = -me[10] * me[1] + me[2] * me[9];
        te[2] = me[6] * me[1] - me[2] * me[5];
        te[3] = -me[10] * me[4] + me[6] * me[8];
        te[4] = me[10] * me[0] - me[2] * me[8];
        te[5] = -me[6] * me[0] + me[2] * me[4];
        te[6] = me[9] * me[4] - me[5] * me[8];
        te[7] = -me[9] * me[0] + me[1] * me[8];
        te[8] = me[5] * me[0] - me[1] * me[4];

        const det = me[0] * te[0] + me[1] * te[3] + me[2] * te[6];

        // no inverse
        if (det === 0) {
          const msg = "Matrix3.getInverse(): can't invert matrix, determinant is 0";

          if (throwOnInvertible || false) {
            throw new Error(msg);
          } else {
            console.warn(msg);
          }

          this.identity();

          return this;
        }

        this.multiplyScalar(1.0 / det);

        return this;
      }

      // https://en.wikipedia.org/wiki/Determinant
      getDeterminant() {
        const m = this.elements;

        /*
         * |a b c| |d e f| |g h i|
         */
        const determinant =
          m[0] * m[4] * m[8] + // +aei
          m[1] * m[5] * m[6] + // +bfg
          m[2] * m[3] * m[7] - // +cdh
          m[2] * m[4] * m[6] - // -ceg
          m[1] * m[3] * m[8] - // -bdi
          m[0] * m[5] * m[7]; // -afh
        return determinant;
      }

      transpose() {
        let tmp: any;
        const m = this.elements;

        tmp = m[1];
        m[1] = m[3];
        m[3] = tmp;
        tmp = m[2];
        m[2] = m[6];
        m[6] = tmp;
        tmp = m[5];
        m[5] = m[7];
        m[7] = tmp;

        return this;
      }

      clone() {
        const te = this.elements;

        return new Matrix3(
          te[0],
          te[3],
          te[6],
          te[1],
          te[4],
          te[7],
          te[2],
          te[5],
          te[8]
        );
      }

      getMatrix4() {
        const m = this.elements;
        return new Matrix4(
          m[0],
          m[3],
          m[6],
          0,
          m[1],
          m[4],
          m[7],
          0,
          m[2],
          m[5],
          m[8],
          0
        );
      }
    }

    /** @class
     *  @subcategory  Math
     * */
    export class Ray {
      origin: Vector3;
      direction: Vector3;

      constructor(origin?: Vector3, direction?: Vector3) {
        this.origin = origin !== undefined ? origin : new Vector3();
        this.direction = direction !== undefined ? direction : new Vector3();
      }

      set(origin: Vector3, direction: Vector3) {
        this.origin.copy(origin);
        this.direction.copy(direction);

        return this;
      }

      copy(ray: Ray) {
        this.origin.copy(ray.origin);
        this.direction.copy(ray.direction);

        return this;
      }

      at(t: number, optionalTarget: Vector3) {
        const result = optionalTarget || new Vector3();

        return result.copy(this.direction).multiplyScalar(t).add(this.origin);
      }

      recast(t: any) {
        const v1 = x;
        this.origin.copy(this.at(t, v1));

        return this;
      }

      closestPointToPoint(point: Vector3, optionalTarget: Vector3) {
        const result = optionalTarget || new Vector3();
        result.subVectors(point, this.origin);
        const directionDistance = result.dot(this.direction);

        // returns a point on this ray
        return result
          .copy(this.direction)
          .multiplyScalar(directionDistance)
          .add(this.origin);
      }

      distanceToPoint(point: Vector3) {
        const v1 = x;
        const directionDistance = v1
          .subVectors(point, this.origin)
          .dot(this.direction);
        v1.copy(this.direction).multiplyScalar(directionDistance).add(this.origin);
        return v1.distanceTo(point);
      }

      // eslint-disable-next-line class-methods-use-this
      isIntersectionCylinder() {}

      isIntersectionSphere(sphere: Sphere) {
        return this.distanceToPoint(sphere.center) <= sphere.radius;
      }

      isIntersectionPlane(plane: any) {
        const denominator = plane.normal.dot(this.direction);

        // plane and ray are not perpendicular
        if (denominator !== 0) return true;

        if (plane.distanceToPoint(this.origin) === 0) return true;

        return false;
      }

      distanceToPlane(plane: any) {
        const denominator = plane.normal.dot(this.direction);
        if (denominator === 0) {
          // line is coplanar
          if (plane.distanceToPoint(this.origin) === 0) return 0;

          // ray is parallel
          return undefined;
        }

        const t = -(this.origin.dot(plane.normal) + plane.constant) / denominator;

        return t;
      }

      intersectPlane(plane: any, optionalTarget: any) {
        const t = this.distanceToPlane(plane);

        if (t === undefined) return undefined;

        return this.at(t, optionalTarget);
      }

      applyMatrix4(matrix4: any) {
        this.direction.add(this.origin).applyMatrix4(matrix4);
        this.origin.applyMatrix4(matrix4);
        this.direction.sub(this.origin);

        return this;
      }

      clone() {
        return new Ray().copy(this);
      }
    }