# lib/model_interface.mjs

    import * as tensorflow from '@tensorflow/tfjs';
    /* fix for rollup */
    /* istanbul ignore next */
    const tf = (tensorflow.default) ? tensorflow.default : tensorflow;
    /**
     * Base class for tensorscript models
     * @interface TensorScriptModelInterface
     * @property {Object} settings - tensorflow model hyperparameters
     * @property {Object} model - tensorflow model
     * @property {Object} tf - tensorflow / tensorflow-node / tensorflow-node-gpu
     * @property {Function} reshape - static reshape array function
     * @property {Function} getInputShape - static TensorScriptModelInterface
     */
    export class TensorScriptModelInterface {
      /**
       * @param {Object} options - tensorflow model hyperparameters
       * @param {Object} customTF - custom, overridale tensorflow / tensorflow-node / tensorflow-node-gpu
       * @param {{model:Object,tf:Object,}} properties - extra instance properties
       */
      constructor(options = {}, properties = {}) {
        /** @type {Object} */
        this.settings = options;
        /** @type {Object} */
        this.model = properties.model;
        /** @type {Object} */
        this.tf = properties.tf || tf;
        /** @type {Function} */
        this.reshape = TensorScriptModelInterface.reshape;
        /** @type {Function} */
        this.getInputShape = TensorScriptModelInterface.getInputShape;
        return this;
      }
      /**
       * Reshapes an array
       * @function
       * @example
       * const array = [ 0, 1, 1, 0, ];
       * const shape = [2,2];
       * TensorScriptModelInterface.reshape(array,shape) // =>
       * [
       *   [ 0, 1, ],
       *   [ 1, 0, ],
       * ];
       * @param {Array<number>} array - input array
       * @param {Array<number>} shape - shape array
       * @return {Array<Array<number>>} returns a matrix with the defined shape
       */
      /* istanbul ignore next */
      static reshape(array, shape) {
        const flatArray = flatten(array);

        function product (arr) {
          return arr.reduce((prev, curr) => prev * curr);
        }

        if (!Array.isArray(array) || !Array.isArray(shape)) {
          throw new TypeError('Array expected');
        }

        if (shape.length === 0) {
          throw new DimensionError(0, product(size(array)), '!=');
        }
        let newArray;
        let totalSize = 1;
        const rows = shape[ 0 ];
        for (let sizeIndex = 0; sizeIndex < shape.length; sizeIndex++) {
          totalSize *= shape[sizeIndex];
        }

        if (flatArray.length !== totalSize) {
          throw new DimensionError(
            product(shape),
            product(size(array)),
            '!='
          );
        }

        try {
          newArray = _reshape(flatArray, shape);
        } catch (e) {
          if (e instanceof DimensionError) {
            throw new DimensionError(
              product(shape),
              product(size(array)),
              '!='
            );
          }
          throw e;
        }
        if (newArray.length !== rows) throw new SyntaxError(`specified shape (${shape}) is compatible with input array or length (${array.length})`);

        // console.log({ newArray ,});
        return newArray;
      }
      /**
       * Returns the shape of an input matrix
       * @function
       * @example
       * const input = [
       *   [ 0, 1, ],
       *   [ 1, 0, ],
       * ];
       * TensorScriptModelInterface.getInputShape(input) // => [2,2]
       * @see {https://stackoverflow.com/questions/10237615/get-size-of-dimensions-in-array}
       * @param {Array<Array<number>>} matrix - input matrix
       * @return {Array<number>} returns the shape of a matrix (e.g. [2,2])
       */
      static getInputShape(matrix=[]) {
        if (Array.isArray(matrix) === false || !matrix[ 0 ] || !matrix[ 0 ].length || Array.isArray(matrix[ 0 ]) === false) throw new TypeError('input must be a matrix');
        const dim = [];
        const x_dimensions = matrix[ 0 ].length;
        let vectors = matrix;
        matrix.forEach(vector => {
          if (vector.length !== x_dimensions) throw new SyntaxError('input must have the same length in each row');
        });
        for (;;) {
          dim.push(vectors.length);
          if (Array.isArray(vectors[0])) {
            vectors = vectors[0];
          } else {
            break;
          }
        }
        return dim;
      }
      /**
       * Asynchronously trains tensorflow model, must be implemented by tensorscript class
       * @abstract
       * @param {Array<Array<number>>} x_matrix - independent variables
       * @param {Array<Array<number>>} y_matrix - dependent variables
       * @return {Object} returns trained tensorflow model
       */
      train(x_matrix, y_matrix) {
        throw new ReferenceError('train method is not implemented');
      }
      /**
       * Predicts new dependent variables
       * @abstract
       * @param {Array<Array<number>>|Array<number>} matrix - new test independent variables
       * @return {{data: Promise}} returns tensorflow prediction
       */
      calculate(matrix) {
        throw new ReferenceError('calculate method is not implemented');
      }
      /**
       * Loads a saved tensoflow / keras model
       * @param {Object} options - tensorflow load model options
       * @return {Object} tensorflow model
       */
      async loadModel(options) {
        this.model = await this.tf.loadModel(options);
        return this.model;
      }
      /**
       * Returns prediction values from tensorflow model
       * @param {Array<Array<number>>|Array<number>} input_matrix - new test independent variables
       * @param {Boolean} [options.json=true] - return object instead of typed array
       * @param {Boolean} [options.probability=true] - return real values instead of integers
       * @return {Array<number>|Array<Array<number>>} predicted model values
       */
      async predict(input_matrix, options = {}) {
        if (!input_matrix || Array.isArray(input_matrix)===false) throw new Error('invalid input matrix');
        const x_matrix = (Array.isArray(input_matrix[ 0 ]))
          ? input_matrix
          : [
            input_matrix,
          ];
        const config = Object.assign({
          json: true,
          probability: true,
        }, options);
        return this.calculate(x_matrix)
          .data()
          .then(predictions => {
            if (config.json === false) {
              return predictions;
            } else {
              const shape = [x_matrix.length, this.yShape[ 1 ], ];
              const predictionValues = (options.probability === false) ? Array.from(predictions).map(Math.round) : Array.from(predictions);
              return this.reshape(predictionValues, shape);
            }
          })
          .catch(e => {
            throw e;
          });
      }
    }

    /**
     * Calculate the size of a multi dimensional array.
     * This function checks the size of the first entry, it does not validate
     * whether all dimensions match. (use function `validate` for that) (from math.js)
     * @param {Array} x
     * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js}
     * @ignore
     * @return {Number[]} size
     */
    /* istanbul ignore next */
    export function size (x) {
      let s = [];

      while (Array.isArray(x)) {
        s.push(x.length);
        x = x[0];
      }

      return s;
    }
    /**
     * Iteratively re-shape a multi dimensional array to fit the specified dimensions (from math.js)
     * @param {Array} array           Array to be reshaped
     * @param {Array.<number>} sizes  List of sizes for each dimension
     * @returns {Array}               Array whose data has been formatted to fit the
     *                                specified dimensions
     * @ignore
     * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js}
     */
    /* istanbul ignore next */
    export function _reshape(array, sizes) {
      // testing if there are enough elements for the requested shape
      var tmpArray = array;
      var tmpArray2;
      // for each dimensions starting by the last one and ignoring the first one
      for (var sizeIndex = sizes.length - 1; sizeIndex > 0; sizeIndex--) {
        var size = sizes[sizeIndex];
        tmpArray2 = [];

        // aggregate the elements of the current tmpArray in elements of the requested size
        var length = tmpArray.length / size;
        for (var i = 0; i < length; i++) {
          tmpArray2.push(tmpArray.slice(i * size, (i + 1) * size));
        }
        // set it as the new tmpArray for the next loop turn or for return
        tmpArray = tmpArray2;
      }
      return tmpArray;
    }

    /**
     * Create a range error with the message:
     *     'Dimension mismatch (<actual size> != <expected size>)' (from math.js)
     * @param {number | number[]} actual        The actual size
     * @param {number | number[]} expected      The expected size
     * @param {string} [relation='!=']          Optional relation between actual
     *                                          and expected size: '!=', '<', etc.
     * @extends RangeError
     * @ignore
     * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js}
     */
    /* istanbul ignore next */
    export class DimensionError extends RangeError {
      constructor(actual, expected, relation) {
        /* istanbul ignore next */
        const message = 'Dimension mismatch (' + (Array.isArray(actual) ? ('[' + actual.join(', ') + ']') : actual) + ' ' + ('!=') + ' ' + (Array.isArray(expected) ? ('[' + expected.join(', ') + ']') : expected) +  ')';
        super(message);

        this.actual = actual;
        this.expected = expected;
        this.relation = relation;
        // this.stack = (new Error()).stack
        this.message = message;
        this.name = 'DimensionError';
        this.isDimensionError = true;
      }
    }

    /**
     * Flatten a multi dimensional array, put all elements in a one dimensional
     * array
     * @param {Array} array   A multi dimensional array
     * @ignore
     * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js}
     * @return {Array}        The flattened array (1 dimensional)
     */
    /* istanbul ignore next */
    export function flatten (array) {
      /* istanbul ignore next */
      if (!Array.isArray(array)) {
        // if not an array, return as is
        /* istanbul ignore next */
        return array;
      }
      let flat = [];

      /* istanbul ignore next */
      array.forEach(function callback (value) {
        if (Array.isArray(value)) {
          value.forEach(callback); // traverse through sub-arrays recursively
        } else {
          flat.push(value);
        }
      });

      return flat;
    }