import nj from '@d4c/numjs/build/module/numjs.min.js';
import { Vector } from '../p5.Vector.js';
import { MatrixInterface } from './MatrixInterface.js';
import '../../constants-DFRExFht.js';

let isMatrixArray = x => Array.isArray(x);
if (typeof Float32Array !== 'undefined') {
  isMatrixArray = x => Array.isArray(x) || x instanceof Float32Array;
}

/**
 * A class to describe a matrix, which can be either a 3×3 or 4×4 matrix,
 * for various matrix manipulations in the p5.js webgl renderer.
 * This class provides methods for common matrix operations such as
 * multiplication, inversion, transposition, and transformation.
 * It supports both 3×3 matrices, typically used for normal transformations,
 * and 4×4 matrices, commonly used for model, view, and projection transformations.
 *
 * @class MatrixNumjs
 * @private
 * @param {Array} [mat4] column-major array literal of our 4×4 matrix
 * @param {Array} [mat3] column-major array literal of our 3×3 matrix
 * @example
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   background(200);
 *
 *   // Create Vector objects.
 *   let p1 = createMatrix(1,1,1,1,1,1,1,1,1);
 *   console.log(p1);
 * }
 */
// const matrixEngine = "numjs";
class MatrixNumjs extends MatrixInterface{
  constructor(...args) {
    // This is default behavior when object
    super(...args);

    if (args[0] === 3) {
      this._mat3 = Array.isArray(args[1]) ? nj.array(args[1]) : nj.identity(3);
    } else {
      this._mat4 = Array.isArray(args[0]) ? nj.array(args[0]) : nj.identity(4);
    }
    return this;
  }

  get mat3() {
    return this._mat3?.flatten().tolist();
  }
  get mat4() {
    return this._mat4?.flatten().tolist();
  }
  /**
   * Resets the matrix to the identity matrix.
   *
   * If the matrix is a 3x3 matrix (`mat3`), it sets the matrix to:
   * [1, 0, 0,
   *  0, 1, 0,
   *  0, 0, 1]
   *
   * If the matrix is a 4x4 matrix (`mat4`), it sets the matrix to:
   * [1, 0, 0, 0,
   *  0, 1, 0, 0,
   *  0, 0, 1, 0,
   *  0, 0, 0, 1]
   *
   * @returns {this} The current instance for chaining.
   */
  reset() {
    if (this._mat3) {
      this._mat3 = nj.identity(3).flatten();
    } else if (this._mat4) {
      this._mat4 = nj.identity(4).flatten();
    }
    return this;
  }

  /**
   * Replace the entire contents of a 4x4 matrix.
   * If providing an array or a MatrixNumjs, the values will be copied without
   * referencing the source object.
   * Can also provide 16 numbers as individual arguments.
   *
   * @param {MatrixNumjs|Float32Array|Number[]} [inMatrix] the input MatrixNumjs or
   *                                     an Array of length 16
   * @chainable
   */
  /**
   * @param {Number[]} elements 16 numbers passed by value to avoid
   *                                     array copying.
   * @chainable
   */
  set(inMatrix) {
    let refArray = [...arguments];
    if (inMatrix instanceof MatrixNumjs) {
      refArray = inMatrix.mat4;
    } else if (isMatrixArray(inMatrix)) {
      refArray = inMatrix;
    }
    if (refArray.length !== 16) {
      // p5._friendlyError(
      //   `Expected 16 values but received ${refArray.length}.`,
      //   "MatrixNumjs.set"
      // );
      return this;
    }
    this._mat4 = nj.array(refArray);
    return this;
  }

  setElement(index, value) {
    if (this._mat3) {
      this._mat3.set(index, value);
    }
    return this;
  }


  /**
   * Gets a copy of the vector, returns a MatrixNumjs object.
   *
   * @return {MatrixNumjs} the copy of the MatrixNumjs object
   */
  get() {
    new MatrixNumjs(this.mat4);
    return new MatrixNumjs(this.mat4);
  }

  /**
   * return a copy of this matrix.
   * If this matrix is 4x4, a 4x4 matrix with exactly the same entries will be
   * generated. The same is true if this matrix is 3x3.
   *
   * @return {MatrixNumjs}   the result matrix
   */
  copy() {
    if (this._mat3 !== undefined) {
      const copied3x3 = new MatrixNumjs(3, this._mat3.tolist());
      copied3x3._mat3 = copied3x3._mat3.flatten();
      return copied3x3;
    }
    const copied = new MatrixNumjs(this._mat4.tolist());
    // copied.set(this);
    copied._mat4 = copied._mat4.flatten();
    return copied;
  }

  /**
   * Creates a copy of the current matrix.
   *
   * @returns {MatrixNumjs} A new matrix that is a copy of the current matrix.
   */
  clone() {
    return this.copy();
  }

  /**
   * return an identity matrix
   * @return {MatrixNumjs}   the result matrix
   */
  static identity(pInst) {
    return new MatrixNumjs(pInst);
  }

  /**
   * transpose according to a given matrix
   * @param  {MatrixNumjs|Float32Array|Number[]} a  the matrix to be
   *                                               based on to transpose
   * @chainable
   */
  transpose(a) {
    if (a instanceof MatrixNumjs) {
      if (a._mat3) {
        this._mat3 = nj.array(a.mat3).reshape(3, 3).transpose().flatten();
      } else if (a._mat4) {
        this._mat4 = nj.array(a.mat4).reshape(4, 4).transpose().flatten();
      }
    } else if (isMatrixArray(a)) {
      if (a.length === 9) {
        let temp3 = new MatrixNumjs(3, a);
        this._mat3 = nj.array(temp3.mat3).reshape(3, 3).transpose().flatten();
      } else if (a.length === 16) {
        let temp4 = new MatrixNumjs(a);
        this._mat4 = nj.array(temp4.mat4).reshape(4, 4).transpose().flatten();
      }
    }
    return this;
  }

  /**
   * invert  matrix according to a give matrix
   * @param  {MatrixNumjs|Float32Array|Number[]} a   the matrix to be
   *                                                based on to invert
   * @chainable
   */
  invert(a) {
    let a00, a01, a02, a03, a10, a11, a12, a13;
    let a20, a21, a22, a23, a30, a31, a32, a33;
    if (a instanceof MatrixNumjs) {
      a00 = a._mat4.get(0);
      a01 = a._mat4.get(1);
      a02 = a._mat4.get(2);
      a03 = a._mat4.get(3);
      a10 = a._mat4.get(4);
      a11 = a._mat4.get(5);
      a12 = a._mat4.get(6);
      a13 = a._mat4.get(7);
      a20 = a._mat4.get(8);
      a21 = a._mat4.get(9);
      a22 = a._mat4.get(10);
      a23 = a._mat4.get(11);
      a30 = a._mat4.get(12);
      a31 = a._mat4.get(13);
      a32 = a._mat4.get(14);
      a33 = a._mat4.get(15);
    } else if (isMatrixArray(a)) {
      a00 = a[0];
      a01 = a[1];
      a02 = a[2];
      a03 = a[3];
      a10 = a[4];
      a11 = a[5];
      a12 = a[6];
      a13 = a[7];
      a20 = a[8];
      a21 = a[9];
      a22 = a[10];
      a23 = a[11];
      a30 = a[12];
      a31 = a[13];
      a32 = a[14];
      a33 = a[15];
    }
    const b00 = a00 * a11 - a01 * a10;
    const b01 = a00 * a12 - a02 * a10;
    const b02 = a00 * a13 - a03 * a10;
    const b03 = a01 * a12 - a02 * a11;
    const b04 = a01 * a13 - a03 * a11;
    const b05 = a02 * a13 - a03 * a12;
    const b06 = a20 * a31 - a21 * a30;
    const b07 = a20 * a32 - a22 * a30;
    const b08 = a20 * a33 - a23 * a30;
    const b09 = a21 * a32 - a22 * a31;
    const b10 = a21 * a33 - a23 * a31;
    const b11 = a22 * a33 - a23 * a32;

    // Calculate the determinant
    let det =
      b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;

    if (!det) {
      return null;
    }
    det = 1.0 / det;

    this._mat4.set(0, (a11 * b11 - a12 * b10 + a13 * b09) * det);
    this._mat4.set(1, (a02 * b10 - a01 * b11 - a03 * b09) * det);
    this._mat4.set(2, (a31 * b05 - a32 * b04 + a33 * b03) * det);
    this._mat4.set(3, (a22 * b04 - a21 * b05 - a23 * b03) * det);
    this._mat4.set(4, (a12 * b08 - a10 * b11 - a13 * b07) * det);
    this._mat4.set(5, (a00 * b11 - a02 * b08 + a03 * b07) * det);
    this._mat4.set(6, (a32 * b02 - a30 * b05 - a33 * b01) * det);
    this._mat4.set(7, (a20 * b05 - a22 * b02 + a23 * b01) * det);
    this._mat4.set(8, (a10 * b10 - a11 * b08 + a13 * b06) * det);
    this._mat4.set(9, (a01 * b08 - a00 * b10 - a03 * b06) * det);
    this._mat4.set(10, (a30 * b04 - a31 * b02 + a33 * b00) * det);
    this._mat4.set(11, (a21 * b02 - a20 * b04 - a23 * b00) * det);
    this._mat4.set(12, (a11 * b07 - a10 * b09 - a12 * b06) * det);
    this._mat4.set(13, (a00 * b09 - a01 * b07 + a02 * b06) * det);
    this._mat4.set(14, (a31 * b01 - a30 * b03 - a32 * b00) * det);
    this._mat4.set(15, (a20 * b03 - a21 * b01 + a22 * b00) * det);

    return this;
  }

  /**
   * Inverts a 3×3 matrix
   * @chainable
   */
  invert3x3() {
    const a00 = this._mat3.get(0);
    const a01 = this._mat3.get(1);
    const a02 = this._mat3.get(2);
    const a10 = this._mat3.get(3);
    const a11 = this._mat3.get(4);
    const a12 = this._mat3.get(5);
    const a20 = this._mat3.get(6);
    const a21 = this._mat3.get(7);
    const a22 = this._mat3.get(8);
    const b01 = a22 * a11 - a12 * a21;
    const b11 = -a22 * a10 + a12 * a20;
    const b21 = a21 * a10 - a11 * a20;

    // Calculate the determinant
    let det = a00 * b01 + a01 * b11 + a02 * b21;
    if (!det) {
      return null;
    }
    det = 1.0 / det;
    this._mat3.set(0, b01 * det);
    this._mat3.set(1, (-a22 * a01 + a02 * a21) * det);
    this._mat3.set(2, (a12 * a01 - a02 * a11) * det);
    this._mat3.set(3, b11 * det);
    this._mat3.set(4, (a22 * a00 - a02 * a20) * det);
    this._mat3.set(5, (-a12 * a00 + a02 * a10) * det);
    this._mat3.set(6, b21 * det);
    this._mat3.set(7, (-a21 * a00 + a01 * a20) * det);
    this._mat3.set(8, (a11 * a00 - a01 * a10) * det);
    return this;
  }

  /**
   * This function is only for 3x3 matrices.
   * transposes a 3×3 MatrixNumjs by a mat3
   * If there is an array of arguments, the matrix obtained by transposing
   * the 3x3 matrix generated based on that array is set.
   * If no arguments, it transposes itself and returns it.
   *
   * @param  {Number[]} mat3 1-dimensional array
   * @chainable
   */
  transpose3x3(mat3) {
    if (mat3 === undefined) {
      mat3 = this._mat3;
      this._mat3 = this._mat3.reshape(3, 3).transpose().flatten();
    } else {
      const temp = new MatrixNumjs(3, mat3);
      temp._mat3 = temp._mat3.reshape(3, 3).transpose().flatten();
      this._mat3 = temp._mat3;
    }
    return this;
  }

  /**
   * converts a 4×4 matrix to its 3×3 inverse transform
   * commonly used in MVMatrix to NMatrix conversions.
   * @param  {MatrixNumjs} mat4 the matrix to be based on to invert
   * @chainable
   * @todo  finish implementation
   */
  inverseTranspose({ mat4 }) {
    if (this._mat3 === undefined) ; else {
      //convert mat4 -> mat3
      this._mat3 = this._mat3.flatten();
      this._mat3.set(0, mat4[0]);
      this._mat3.set(1, mat4[1]);
      this._mat3.set(2, mat4[2]);
      this._mat3.set(3, mat4[4]);
      this._mat3.set(4, mat4[5]);
      this._mat3.set(5, mat4[6]);
      this._mat3.set(6, mat4[8]);
      this._mat3.set(7, mat4[9]);
      this._mat3.set(8, mat4[10]);
    }

    const inverse = this.invert3x3();
    // check inverse succeeded
    if (inverse) {
      inverse.transpose3x3(this._mat3);
    } else {
      // in case of singularity, just zero the matrix
      for (let i = 0; i < 9; i++) {
        this._mat3.set(i, 0);
      }
    }
    return this;
  }

  /**
   * inspired by Toji's mat4 determinant
   * @return {Number} Determinant of our 4×4 matrix
   */
  determinant() {
    const d00 =
        this._mat4.get(0) * this._mat4.get(5) -
        this._mat4.get(1) * this._mat4.get(4),
      d01 =
        this._mat4.get(0) * this._mat4.get(6) -
        this._mat4.get(2) * this._mat4.get(4),
      d02 =
        this._mat4.get(0) * this._mat4.get(7) -
        this._mat4.get(3) * this._mat4.get(4),
      d03 =
        this._mat4.get(1) * this._mat4.get(6) -
        this._mat4.get(2) * this._mat4.get(5),
      d04 =
        this._mat4.get(1) * this._mat4.get(7) -
        this._mat4.get(3) * this._mat4.get(5),
      d05 =
        this._mat4.get(2) * this._mat4.get(7) -
        this._mat4.get(3) * this._mat4.get(6),
      d06 =
        this._mat4.get(8) * this._mat4.get(13) -
        this._mat4.get(9) * this._mat4.get(12),
      d07 =
        this._mat4.get(8) * this._mat4.get(14) -
        this._mat4.get(10) * this._mat4.get(12),
      d08 =
        this._mat4.get(8) * this._mat4.get(15) -
        this._mat4.get(11) * this._mat4.get(12),
      d09 =
        this._mat4.get(9) * this._mat4.get(14) -
        this._mat4.get(10) * this._mat4.get(13),
      d10 =
        this._mat4.get(9) * this._mat4.get(15) -
        this._mat4.get(11) * this._mat4.get(13),
      d11 =
        this._mat4.get(10) * this._mat4.get(15) -
        this._mat4.get(11) * this._mat4.get(14);

    // Calculate the determinant
    return (
      d00 * d11 - d01 * d10 + d02 * d09 + d03 * d08 - d04 * d07 + d05 * d06
    );
  }

  /**
   * multiply two mat4s
   * @param {MatrixNumjs|Float32Array|Number[]} multMatrix The matrix
   *                                                we want to multiply by
   * @chainable
   */
  mult(multMatrix) {
    if (isMatrixArray(multMatrix)) {
      multMatrix = new MatrixNumjs(multMatrix);
    }
    if (this._mat3 !== undefined) {
      let a = this._mat3.reshape(3, 3);
      a = a.dot(multMatrix._mat3?.reshape(3, 3)).flatten();
      this._mat3 = a;
    } else if (this._mat4 !== undefined) {
      let a = this._mat4.reshape(4, 4);
      a = a.dot(multMatrix._mat4?.reshape(4, 4)).flatten();
      this._mat4 = a;
    }
    return this;
  }

  apply(multMatrix) {
    let _src;

    if (multMatrix === this || multMatrix === this._mat4) {
      _src = this.copy().mat4; // only need to allocate in this rare case
    } else if (multMatrix instanceof MatrixNumjs) {
      _src = multMatrix.mat4;
    } else if (isMatrixArray(multMatrix)) {
      _src = multMatrix;
    } else if (arguments.length === 16) {
      _src = arguments;
    } else {
      return; // nothing to do.
    }

    const mat4 = this._mat4.tolist();

    // each row is used for the multiplier
    const m0 = mat4[0];
    const m4 = mat4[4];
    const m8 = mat4[8];
    const m12 = mat4[12];
    mat4[0] = _src[0] * m0 + _src[1] * m4 + _src[2] * m8 + _src[3] * m12;
    mat4[4] = _src[4] * m0 + _src[5] * m4 + _src[6] * m8 + _src[7] * m12;
    mat4[8] = _src[8] * m0 + _src[9] * m4 + _src[10] * m8 + _src[11] * m12;
    mat4[12] = _src[12] * m0 + _src[13] * m4 + _src[14] * m8 + _src[15] * m12;

    const m1 = mat4[1];
    const m5 = mat4[5];
    const m9 = mat4[9];
    const m13 = mat4[13];
    mat4[1] = _src[0] * m1 + _src[1] * m5 + _src[2] * m9 + _src[3] * m13;
    mat4[5] = _src[4] * m1 + _src[5] * m5 + _src[6] * m9 + _src[7] * m13;
    mat4[9] = _src[8] * m1 + _src[9] * m5 + _src[10] * m9 + _src[11] * m13;
    mat4[13] = _src[12] * m1 + _src[13] * m5 + _src[14] * m9 + _src[15] * m13;

    const m2 = mat4[2];
    const m6 = mat4[6];
    const m10 = mat4[10];
    const m14 = mat4[14];
    mat4[2] = _src[0] * m2 + _src[1] * m6 + _src[2] * m10 + _src[3] * m14;
    mat4[6] = _src[4] * m2 + _src[5] * m6 + _src[6] * m10 + _src[7] * m14;
    mat4[10] = _src[8] * m2 + _src[9] * m6 + _src[10] * m10 + _src[11] * m14;
    mat4[14] = _src[12] * m2 + _src[13] * m6 + _src[14] * m10 + _src[15] * m14;

    const m3 = mat4[3];
    const m7 = mat4[7];
    const m11 = mat4[11];
    const m15 = mat4[15];
    mat4[3] = _src[0] * m3 + _src[1] * m7 + _src[2] * m11 + _src[3] * m15;
    mat4[7] = _src[4] * m3 + _src[5] * m7 + _src[6] * m11 + _src[7] * m15;
    mat4[11] = _src[8] * m3 + _src[9] * m7 + _src[10] * m11 + _src[11] * m15;
    mat4[15] = _src[12] * m3 + _src[13] * m7 + _src[14] * m11 + _src[15] * m15;
    this._mat4 = nj.array(mat4);
    return this;
  }

  /**
   * scales a MatrixNumjs by scalars or a vector
   * @param  {Vector|Float32Array|Number[]} s vector to scale by
   * @chainable
   */
  scale(x, y, z) {
    if (x instanceof Vector) {
      // x is a vector, extract the components from it.
      y = x.y;
      z = x.z;
      x = x.x; // must be last
    } else if (x instanceof Array) {
      // x is an array, extract the components from it.
      y = x[1];
      z = x[2];
      x = x[0]; // must be last
    }
    this._mat4 = this._mat4.flatten();
    nj.array([x, y, z, 1]);
    this._mat4.set(0, x * this._mat4.get(0));
    this._mat4.set(1, x * this._mat4.get(1));
    this._mat4.set(2, x * this._mat4.get(2));
    this._mat4.set(3, x * this._mat4.get(3));
    this._mat4.set(4, y * this._mat4.get(4));
    this._mat4.set(5, y * this._mat4.get(5));
    this._mat4.set(6, y * this._mat4.get(6));
    this._mat4.set(7, y * this._mat4.get(7));
    this._mat4.set(8, z * this._mat4.get(8));
    this._mat4.set(9, z * this._mat4.get(9));
    this._mat4.set(10, z * this._mat4.get(10));
    this._mat4.set(11, z * this._mat4.get(11));
    return this;
  }

  /**
   * rotate our Matrix around an axis by the given angle.
   * @param  {Number} a The angle of rotation in radians
   * @param  {Vector|Number[]} axis  the axis(es) to rotate around
   * @chainable
   * inspired by Toji's gl-matrix lib, mat4 rotation
   */
  rotate(a, x, y, z) {
    if (x instanceof Vector) {
      // x is a vector, extract the components from it.
      y = x.y;
      z = x.z;
      x = x.x; //must be last
    } else if (x instanceof Array) {
      // x is an array, extract the components from it.
      y = x[1];
      z = x[2];
      x = x[0]; //must be last
    }

    const len = Math.sqrt(x * x + y * y + z * z);
    x *= 1 / len;
    y *= 1 / len;
    z *= 1 / len;

    // const aMat = this._mat4.reshape(4,4)
    this._mat4 = this._mat4.flatten();
    const a00 = this._mat4.get(0);
    const a01 = this._mat4.get(1);
    const a02 = this._mat4.get(2);
    const a03 = this._mat4.get(3);
    const a10 = this._mat4.get(4);
    const a11 = this._mat4.get(5);
    const a12 = this._mat4.get(6);
    const a13 = this._mat4.get(7);
    const a20 = this._mat4.get(8);
    const a21 = this._mat4.get(9);
    const a22 = this._mat4.get(10);
    const a23 = this._mat4.get(11);

    //sin,cos, and tan of respective angle
    const sA = Math.sin(a);
    const cA = Math.cos(a);
    const tA = 1 - cA;
    // Construct the elements of the rotation matrix
    const b00 = x * x * tA + cA;
    const b01 = y * x * tA + z * sA;
    const b02 = z * x * tA - y * sA;

    const b10 = x * y * tA - z * sA;
    const b11 = y * y * tA + cA;
    const b12 = z * y * tA + x * sA;

    const b20 = x * z * tA + y * sA;
    const b21 = y * z * tA - x * sA;
    const b22 = z * z * tA + cA;

    // rotation-specific matrix multiplication
    this._mat4.set(0, a00 * b00 + a10 * b01 + a20 * b02);
    this._mat4.set(1, a01 * b00 + a11 * b01 + a21 * b02);
    this._mat4.set(2, a02 * b00 + a12 * b01 + a22 * b02);
    this._mat4.set(3, a03 * b00 + a13 * b01 + a23 * b02);
    this._mat4.set(4, a00 * b10 + a10 * b11 + a20 * b12);
    this._mat4.set(5, a01 * b10 + a11 * b11 + a21 * b12);
    this._mat4.set(6, a02 * b10 + a12 * b11 + a22 * b12);
    this._mat4.set(7, a03 * b10 + a13 * b11 + a23 * b12);
    this._mat4.set(8, a00 * b20 + a10 * b21 + a20 * b22);
    this._mat4.set(9, a01 * b20 + a11 * b21 + a21 * b22);
    this._mat4.set(10, a02 * b20 + a12 * b21 + a22 * b22);
    this._mat4.set(11, a03 * b20 + a13 * b21 + a23 * b22);
    return this;
  }

  /**
   * @todo  finish implementing this method!
   * translates
   * @param  {Number[]} v vector to translate by
   * @chainable
   */
  translate(v) {
    const x = v[0],
      y = v[1],
      z = v[2] || 0;
    this._mat4 = this._mat4.flatten();
    this._mat4.set(
      12,
      this._mat4.get(0) * x +
        this._mat4.get(4) * y +
        this._mat4.get(8) * z +
        this._mat4.get(12)
    );
    this._mat4.set(
      13,
      this._mat4.get(1) * x +
        this._mat4.get(5) * y +
        this._mat4.get(9) * z +
        this._mat4.get(13)
    );
    this._mat4.set(
      14,
      this._mat4.get(2) * x +
        this._mat4.get(6) * y +
        this._mat4.get(10) * z +
        this._mat4.get(14)
    );
    this._mat4.set(
      15,
      this._mat4.get(3) * x +
        this._mat4.get(7) * y +
        this._mat4.get(11) * z +
        this._mat4.get(15)
    );
  }

  rotateX(a) {
    this.rotate(a, 1, 0, 0);
  }
  rotateY(a) {
    this.rotate(a, 0, 1, 0);
  }
  rotateZ(a) {
    this.rotate(a, 0, 0, 1);
  }

  /**
   * sets the perspective matrix
   * @param  {Number} fovy   [description]
   * @param  {Number} aspect [description]
   * @param  {Number} near   near clipping plane
   * @param  {Number} far    far clipping plane
   * @chainable
   */
  perspective(fovy, aspect, near, far) {
    const f = 1.0 / Math.tan(fovy / 2),
      nf = 1 / (near - far);

    this._mat4 = this._mat4.flatten();
    this._mat4.set(0, f / aspect);
    this._mat4.set(1, 0);
    this._mat4.set(2, 0);
    this._mat4.set(3, 0);
    this._mat4.set(4, 0);
    this._mat4.set(5, f);
    this._mat4.set(6, 0);
    this._mat4.set(7, 0);
    this._mat4.set(8, 0);
    this._mat4.set(9, 0);
    this._mat4.set(10, (far + near) * nf);
    this._mat4.set(11, -1);
    this._mat4.set(12, 0);
    this._mat4.set(13, 0);
    this._mat4.set(14, 2 * far * near * nf);
    this._mat4.set(15, 0);

    return this;
  }

  /**
   * sets the ortho matrix
   * @param  {Number} left   [description]
   * @param  {Number} right  [description]
   * @param  {Number} bottom [description]
   * @param  {Number} top    [description]
   * @param  {Number} near   near clipping plane
   * @param  {Number} far    far clipping plane
   * @chainable
   */
  ortho(left, right, bottom, top, near, far) {
    const lr = 1 / (left - right),
      bt = 1 / (bottom - top),
      nf = 1 / (near - far);
    this._mat4 = this._mat4.flatten();
    this._mat4.set(0, -2 * lr);
    this._mat4.set(1, 0);
    this._mat4.set(2, 0);
    this._mat4.set(3, 0);
    this._mat4.set(4, 0);
    this._mat4.set(5, -2 * bt);
    this._mat4.set(6, 0);
    this._mat4.set(7, 0);
    this._mat4.set(8, 0);
    this._mat4.set(9, 0);
    this._mat4.set(10, 2 * nf);
    this._mat4.set(11, 0);
    this._mat4.set(12, (left + right) * lr);
    this._mat4.set(13, (top + bottom) * bt);
    this._mat4.set(14, (far + near) * nf);
    this._mat4.set(15, 1);

    return this;
  }

  /**
   * apply a matrix to a vector with x,y,z,w components
   * get the results in the form of an array
   * @param {Number}
   * @return {Number[]}
   */
  multiplyVec4(x, y, z, w) {
    const result = new Array(4);
    const m = this._mat4;

    result[0] = m.get(0) * x + m.get(4) * y + m.get(8) * z + m.get(12) * w;
    result[1] = m.get(1) * x + m.get(5) * y + m.get(9) * z + m.get(13) * w;
    result[2] = m.get(2) * x + m.get(6) * y + m.get(10) * z + m.get(14) * w;
    result[3] = m.get(3) * x + m.get(7) * y + m.get(11) * z + m.get(15) * w;

    return result;
  }

  /**
   * Applies a matrix to a vector.
   * The fourth component is set to 1.
   * Returns a vector consisting of the first
   * through third components of the result.
   *
   * @param {Vector}
   * @return {Vector}
   */
  multiplyPoint({ x, y, z }) {
    const array = this.multiplyVec4(x, y, z, 1);
    return new Vector(array[0], array[1], array[2]);
  }

  /**
   * Applies a matrix to a vector.
   * The fourth component is set to 1.
   * Returns the result of dividing the 1st to 3rd components
   * of the result by the 4th component as a vector.
   *
   * @param {Vector}
   * @return {Vector}
   */
  multiplyAndNormalizePoint({ x, y, z }) {
    const array = this.multiplyVec4(x, y, z, 1);
    array[0] /= array[3];
    array[1] /= array[3];
    array[2] /= array[3];
    return new Vector(array[0], array[1], array[2]);
  }

  /**
   * Applies a matrix to a vector.
   * The fourth component is set to 0.
   * Returns a vector consisting of the first
   * through third components of the result.
   *
   * @param {Vector}
   * @return {Vector}
   */
  multiplyDirection({ x, y, z }) {
    const array = this.multiplyVec4(x, y, z, 0);
    return new Vector(array[0], array[1], array[2]);
  }

  /**
   * This function is only for 3x3 matrices.
   * multiply two mat3s. It is an operation to multiply the 3x3 matrix of
   * the argument from the right. Arguments can be a 3x3 MatrixNumjs,
   * a Float32Array of length 9, or a javascript array of length 9.
   * In addition, it can also be done by enumerating 9 numbers.
   *
   * @param {MatrixNumjs|Float32Array|Number[]} multMatrix The matrix
   *                                                we want to multiply by
   * @chainable
   */
  mult3x3(multMatrix) {
    let tempMatrix = multMatrix;
    if (multMatrix === this || multMatrix === this._mat3) ; else if (multMatrix instanceof MatrixNumjs) {
      multMatrix.mat3;
    } else if (isMatrixArray(multMatrix)) {
      multMatrix._mat3 = nj.array(arguments);
    } else if (arguments.length === 9) {
      tempMatrix = new MatrixNumjs(3, Array.from(arguments));
    } else {
      return; // nothing to do.
    }
    let a = this._mat3.reshape(3, 3);
    a = a.dot(tempMatrix._mat3.reshape(3, 3)).flatten();
    this._mat3 = a;
    return this;
  }

  /**
   * This function is only for 3x3 matrices.
   * A function that returns a column vector of a 3x3 matrix.
   *
   * @param {Number} columnIndex matrix column number
   * @return {Vector}
   */
  column(columnIndex) {
    // let temp = this._mat3.reshape(3,3)
    let vect = new Vector(
      this._mat3.tolist()[3 * columnIndex],
      this._mat3.tolist()[3 * columnIndex + 1],
      this._mat3.tolist()[3 * columnIndex + 2]
    );
    return vect;
  }

  /**
   * This function is only for 3x3 matrices.
   * A function that returns a row vector of a 3x3 matrix.
   *
   * @param {Number} rowIndex matrix row number
   * @return {Vector}
   */
  row(rowIndex) {
    return new Vector(
      this._mat3.tolist()[rowIndex],
      this._mat3.tolist()[rowIndex + 3],
      this._mat3.tolist()[rowIndex + 6]
    );
  }

  /**
   * Returns the diagonal elements of the matrix in the form of an array.
   * A 3x3 matrix will return an array of length 3.
   * A 4x4 matrix will return an array of length 4.
   *
   * @return {Number[]} An array obtained by arranging the diagonal elements
   *                    of the matrix in ascending order of index
   */
  diagonal() {
    if (this._mat3 !== undefined) {
      return this._mat3.reshape(3, 3).diag().tolist();
    }
    return this._mat4.reshape(4, 4).diag().tolist();
  }

  /**
   * This function is only for 3x3 matrices.
   * Takes a vector and returns the vector resulting from multiplying to
   * that vector by this matrix from left.
   *
   * @param {Vector} multVector the vector to which this matrix applies
   * @param {Vector} [target] The vector to receive the result
   * @return {Vector}
   */
  multiplyVec3(multVector, target) {
    if (target === undefined) {
      target = multVector.copy();
    }
    target.x = this.row(0).dot(multVector);
    target.y = this.row(1).dot(multVector);
    target.z = this.row(2).dot(multVector);
    return target;
  }

  /**
   * This function is only for 4x4 matrices.
   * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it.
   *
   * @return {MatrixNumjs}
   */
  createSubMatrix3x3() {
    const result = new MatrixNumjs(3);
    result._mat3 = result._mat3.flatten();
    result._mat3.set(0, this._mat4.get(0));
    result._mat3.set(1, this._mat4.get(1));
    result._mat3.set(2, this._mat4.get(2));
    result._mat3.set(3, this._mat4.get(4));
    result._mat3.set(4, this._mat4.get(5));
    result._mat3.set(5, this._mat4.get(6));
    result._mat3.set(6, this._mat4.get(8));
    result._mat3.set(7, this._mat4.get(9));
    result._mat3.set(8, this._mat4.get(10));
    return result;
  }

  /**
   * PRIVATE
   */
  // matrix methods adapted from:
  // https://developer.mozilla.org/en-US/docs/Web/WebGL/
  // gluPerspective
  //
  // function _makePerspective(fovy, aspect, znear, zfar){
  //    const ymax = znear * Math.tan(fovy * Math.PI / 360.0);
  //    const ymin = -ymax;
  //    const xmin = ymin * aspect;
  //    const xmax = ymax * aspect;
  //    return _makeFrustum(xmin, xmax, ymin, ymax, znear, zfar);
  //  }

  ////
  //// glFrustum
  ////
  //function _makeFrustum(left, right, bottom, top, znear, zfar){
  //  const X = 2*znear/(right-left);
  //  const Y = 2*znear/(top-bottom);
  //  const A = (right+left)/(right-left);
  //  const B = (top+bottom)/(top-bottom);
  //  const C = -(zfar+znear)/(zfar-znear);
  //  const D = -2*zfar*znear/(zfar-znear);
  //  const frustrumMatrix =[
  //  X, 0, A, 0,
  //  0, Y, B, 0,
  //  0, 0, C, D,
  //  0, 0, -1, 0
  //];
  //return frustrumMatrix;
  // }

  // function _setMVPMatrices(){
  ////an identity matrix
  ////@TODO use the MatrixNumjs class to abstract away our MV matrices and
  ///other math
  //const _mvMatrix =
  //[
  //  1.0,0.0,0.0,0.0,
  //  0.0,1.0,0.0,0.0,
  //  0.0,0.0,1.0,0.0,
  //  0.0,0.0,0.0,1.0
  //];
}

export { MatrixNumjs };
