# lib/lstm_multivariate_time_series.mjs

    import { LSTMTimeSeries, } from './lstm_time_series';
    import range from 'lodash.range';

    /**
     * Long Short Term Memory Multivariate Time Series with Tensorflow
     * @class LSTMMultivariateTimeSeries
     * @extends {LSTMTimeSeries}
     */
    export class LSTMMultivariateTimeSeries extends LSTMTimeSeries {
      /**
       * Creates dataset data
       * @example
       * const ds = [
      [10, 20, 30, 40, 50, 60, 70, 80, 90,],
      [11, 21, 31, 41, 51, 61, 71, 81, 91,],
      [12, 22, 32, 42, 52, 62, 72, 82, 92,],
      [13, 23, 33, 43, 53, 63, 73, 83, 93,],
      [14, 24, 34, 44, 54, 64, 74, 84, 94,],
      [15, 25, 35, 45, 55, 65, 75, 85, 95,],
      [16, 26, 36, 46, 56, 66, 76, 86, 96,],
      [17, 27, 37, 47, 57, 67, 77, 87, 97,],
      [18, 28, 38, 48, 58, 68, 78, 88, 98,],
      [19, 29, 39, 49, 59, 69, 79, 89, 99,],
    ];
       * LSTMMultivariateTimeSeries.createDataset(ds,1) // =>
          //  [
          //   [
          //    [ 20, 30, 40, 50, 60, 70, 80, 90 ],
          //    [ 21, 31, 41, 51, 61, 71, 81, 91 ],
          //    [ 22, 32, 42, 52, 62, 72, 82, 92 ],
          //    [ 23, 33, 43, 53, 63, 73, 83, 93 ],
          //    [ 24, 34, 44, 54, 64, 74, 84, 94 ],
          //    [ 25, 35, 45, 55, 65, 75, 85, 95 ],
          //    [ 26, 36, 46, 56, 66, 76, 86, 96 ],
          //    [ 27, 37, 47, 57, 67, 77, 87, 97 ],
          //    [ 28, 38, 48, 58, 68, 78, 88, 98 ]
          //   ], //x_matrix
          //   [ 11, 12, 13, 14, 15, 16, 17, 18, 19 ], //y_matrix
          //   8 //features
          // ]
       * @param {Array<Array<number>} dataset - array of values
       * @param {Number} look_back - number of values in each feature
       * @override
       * @return {[Array<Array<number>>,Array<number>]} returns x matrix and y matrix for model trainning
       */
      /* istanbul ignore next */
      static createDataset(dataset = [], look_back = 1) {
        const features = (this.settings && this.settings.features) ? this.settings.features : dataset[ 0 ].length - 1;
        const n_in = look_back || this.settings.lookback; //1; //lookbacks
        const n_out = (this.settings && this.settings.outputs) ? this.settings.outputs : 1; //1;
        const series = LSTMMultivariateTimeSeries.seriesToSupervised(dataset, n_in, n_out);
        const dropped = LSTMMultivariateTimeSeries.getDropableColumns(features, n_in, n_out);
        const droppedColumns = LSTMMultivariateTimeSeries.drop(series, dropped);
        const dropped_c_columns = [ 0, ];
        const original_dropped_c_columns = [ 0, droppedColumns[ 0 ].length - 1, ];
        for (let i=0; i < n_in; i++){
          dropped_c_columns.push((i + 1) * features+1);
        }

        // console.log({ series, dropped_c_columns,original_dropped_c_columns, dropped, });
        const y = pivotVector(droppedColumns)[ droppedColumns[0].length - 1 ];
        const x = LSTMMultivariateTimeSeries.drop(
          droppedColumns,
          original_dropped_c_columns,
          // [ 0, droppedColumns[ 0 ].length - 1, ]
        );
        return [ x, y, features, ];
      }
      /**
       * Drops columns by array index
       * @example
    const data = [ [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 11, 21, 31, 41, 51, 61, 71, 81, 91 ],
         [ 11, 21, 31, 41, 51, 61, 71, 81, 91, 12, 22, 32, 42, 52, 62, 72, 82, 92 ],
         [ 12, 22, 32, 42, 52, 62, 72, 82, 92, 13, 23, 33, 43, 53, 63, 73, 83, 93 ],
         [ 13, 23, 33, 43, 53, 63, 73, 83, 93, 14, 24, 34, 44, 54, 64, 74, 84, 94 ],
         [ 14, 24, 34, 44, 54, 64, 74, 84, 94, 15, 25, 35, 45, 55, 65, 75, 85, 95 ],
         [ 15, 25, 35, 45, 55, 65, 75, 85, 95, 16, 26, 36, 46, 56, 66, 76, 86, 96 ],
         [ 16, 26, 36, 46, 56, 66, 76, 86, 96, 17, 27, 37, 47, 57, 67, 77, 87, 97 ],
         [ 17, 27, 37, 47, 57, 67, 77, 87, 97, 18, 28, 38, 48, 58, 68, 78, 88, 98 ],
         [ 18, 28, 38, 48, 58, 68, 78, 88, 98, 19, 29, 39, 49, 59, 69, 79, 89, 99 ] ];
    const n_in = 1; //lookbacks
    const n_out = 1;
    const dropColumns = getDropableColumns(8, n_in, n_out); // =>[ 10, 11, 12, 13, 14, 15, 16, 17 ]
    const newdata = drop(data,dropColumns); //=> [
        // [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 11 ],
        // [ 11, 21, 31, 41, 51, 61, 71, 81, 91, 12 ],
        // [ 12, 22, 32, 42, 52, 62, 72, 82, 92, 13 ],
        // [ 13, 23, 33, 43, 53, 63, 73, 83, 93, 14 ],
        // [ 14, 24, 34, 44, 54, 64, 74, 84, 94, 15 ],
        // [ 15, 25, 35, 45, 55, 65, 75, 85, 95, 16 ],
        // [ 16, 26, 36, 46, 56, 66, 76, 86, 96, 17 ],
        // [ 17, 27, 37, 47, 57, 67, 77, 87, 97, 18 ],
        // [ 18, 28, 38, 48, 58, 68, 78, 88, 98, 19 ]
        //]
      * @param {Array<Array<number>>} data - data set to drop columns
      * @param {Array<number>} columns - array of column indexes
       * @returns {Array<Array<number>>} matrix with dropped columns
       */
      static drop(data, columns) {
        return data.reduce((cols, row, i) => {
          cols[ i ] = [];
          row.forEach((col, idx) => {
            if (columns.indexOf(idx)===-1) {
              cols[ i ].push(col);
            }
          });
          return cols;
        }, []);
      }
      /**
       * Converts data set to supervised labels for forecasting, the first column must be the dependent variable
       * @example
       const ds = [
        [10, 20, 30, 40, 50, 60, 70, 80, 90,],
        [11, 21, 31, 41, 51, 61, 71, 81, 91,],
        [12, 22, 32, 42, 52, 62, 72, 82, 92,],
        [13, 23, 33, 43, 53, 63, 73, 83, 93,],
        [14, 24, 34, 44, 54, 64, 74, 84, 94,],
        [15, 25, 35, 45, 55, 65, 75, 85, 95,],
        [16, 26, 36, 46, 56, 66, 76, 86, 96,],
        [17, 27, 37, 47, 57, 67, 77, 87, 97,],
        [18, 28, 38, 48, 58, 68, 78, 88, 98,],
        [19, 29, 39, 49, 59, 69, 79, 89, 99,],
      ];
      const n_in = 1; //lookbacks
      const n_out = 1;
      const series = seriesToSupervised(ds, n_in, n_out); //=> [
        // [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 11, 21, 31, 41, 51, 61, 71, 81, 91 ],
        // [ 11, 21, 31, 41, 51, 61, 71, 81, 91, 12, 22, 32, 42, 52, 62, 72, 82, 92 ],
        // [ 12, 22, 32, 42, 52, 62, 72, 82, 92, 13, 23, 33, 43, 53, 63, 73, 83, 93 ],
        // [ 13, 23, 33, 43, 53, 63, 73, 83, 93, 14, 24, 34, 44, 54, 64, 74, 84, 94 ],
        // [ 14, 24, 34, 44, 54, 64, 74, 84, 94, 15, 25, 35, 45, 55, 65, 75, 85, 95 ],
        // [ 15, 25, 35, 45, 55, 65, 75, 85, 95, 16, 26, 36, 46, 56, 66, 76, 86, 96 ],
        // [ 16, 26, 36, 46, 56, 66, 76, 86, 96, 17, 27, 37, 47, 57, 67, 77, 87, 97 ],
        // [ 17, 27, 37, 47, 57, 67, 77, 87, 97, 18, 28, 38, 48, 58, 68, 78, 88, 98 ],
        // [ 18, 28, 38, 48, 58, 68, 78, 88, 98, 19, 29, 39, 49, 59, 69, 79, 89, 99 ]
        //];
       *
       * @param {Array<Array<number>>} data - data set
       * @param {number} n_in - look backs
       * @param {number} n_out - future iterations (only 1 supported)
       * @todo support multiple future iterations
       * @returns {Array<Array<number>>} multivariate dataset for time series
       */
      static seriesToSupervised(data, n_in = 1, n_out = 1) {
        if (n_out !== 1) throw new RangeError('Only 1 future iteration supported');
        if (data && Array.isArray(data) && Array.isArray(data[0]) && data[ 0 ].length < 2) throw new RangeError('Must include at least two columns, the first column must be the dependent variable, followed by independent variables');
        // let n_vars = data[ 0 ].length;
        let cols = [];
        // let names = [];
        // input sequence (t-n, ... t-1)
        for (let x in data) {
          x = parseInt(x);
          let maxIndex = x + n_in + n_out;
          if (maxIndex > data.length) break;
          cols[ x ] = [];
          // console.log({ x,maxIndex });
          for (let i in range(n_in)) {
            i = parseInt(i);
            cols[ x ].push(...data[x+i]);
            // console.log({ i, cols, });
          }
          for (let j in range(n_out)){
            j = parseInt(j);
            cols[ x ].push(...data[ x+j+n_in ]);
            // console.log({ j, cols, });
          }
        }
        return cols;
      }
      /**
       * Calculates which columns to drop by index
       * @todo support multiple iterations in the future, also only one output variable supported in column features * lookbacks -1
       * @example
    const ds = [
      [10, 20, 30, 40, 50, 60, 70, 80, 90,],
      [11, 21, 31, 41, 51, 61, 71, 81, 91,],
      [12, 22, 32, 42, 52, 62, 72, 82, 92,],
      [13, 23, 33, 43, 53, 63, 73, 83, 93,],
      [14, 24, 34, 44, 54, 64, 74, 84, 94,],
      [15, 25, 35, 45, 55, 65, 75, 85, 95,],
      [16, 26, 36, 46, 56, 66, 76, 86, 96,],
      [17, 27, 37, 47, 57, 67, 77, 87, 97,],
      [18, 28, 38, 48, 58, 68, 78, 88, 98,],
      [19, 29, 39, 49, 59, 69, 79, 89, 99,],
    ];
    const n_in = 1; //lookbacks
    const n_out = 1;
    const dropped = getDropableColumns(8, n_in, n_out); //=> [ 10, 11, 12, 13, 14, 15, 16, 17 ]
       * @param {number} features - number of independent variables
       * @param {number} n_in - look backs
       * @param {number} n_out - future iterations (currently only 1 supported)
       * @returns {Array<number>} array indexes to drop
       */
      static getDropableColumns(features, n_in, n_out) {
        if (n_out !== 1) throw new RangeError('Only 1 future iteration supported');
        const cols = features + 1;
        const total_cols = cols * n_in + cols * n_out;
        // console.log({ cols, total_cols });
        return range(total_cols - cols +1, total_cols);
      }
      /**
       * Reshape input to be [samples, time steps, features]
       * @example
       * @override
       * LSTMTimeSeries.getTimeseriesShape([
          [ [ 1 ], [ 2 ], [ 3 ] ],
          [ [ 2 ], [ 3 ], [ 4 ] ],
          [ [ 3 ], [ 4 ], [ 5 ] ],
          [ [ 4 ], [ 5 ], [ 6 ] ],
          [ [ 5 ], [ 6 ], [ 7 ] ],
          [ [ 6 ], [ 7 ], [ 8 ] ],
        ]) //=> [6, 1, 3,]
       * @param {Array<Array<number>} x_timeseries - dataset array of values
       * @return {Array<Array<number>>} returns proper timeseries forecasting shape
       */
      static getTimeseriesShape(x_timeseries) {
        const time_steps = (this.settings && this.settings.lookback) ? this.settings.lookback : 1;
        const xShape = this.getInputShape(x_timeseries);
        const _samples = xShape[ 0 ];
        const _timeSteps = time_steps;
        const _features = (this.settings && this.settings.features) ? this.settings.features : xShape[ 1 ];
        // reshape input to be 3D [samples, timesteps, features]
        // train_X = train_X.reshape((train_X.shape[0], n_hours, n_features))
        const newShape = [ _samples, _timeSteps, _features, ];
        // console.log({newShape})
        return newShape;
      }
      /**
       * Returns data for predicting values
       * @param timeseries
       * @param look_back
       * @override
       */
      static getTimeseriesDataSet(timeseries, look_back) {
        const lookBack = look_back || this.settings.lookback;
        const matrices = LSTMMultivariateTimeSeries.createDataset.call(this, timeseries, lookBack);
        const x_matrix = matrices[ 0 ];
        const y_matrix_m = LSTMMultivariateTimeSeries.reshape(matrices[ 1 ], [ matrices[ 1 ].length, 1 ]);
        const timeseriesShape = LSTMMultivariateTimeSeries.getTimeseriesShape.call(this,x_matrix);
        // const timeseriesShape = LSTMMultivariateTimeSeries.getTimeseriesShape(x_matrix);
        const x_matrix_timeseries = LSTMMultivariateTimeSeries.reshape(x_matrix, timeseriesShape);
        // const x_matrix_timeseries = LSTMMultivariateTimeSeries.reshape(x_matrix, [x_matrix.length, lookBack, ]);
        const xShape = LSTMMultivariateTimeSeries.getInputShape(x_matrix_timeseries);
        const y_matrix = pivotVector(y_matrix_m)[ y_matrix_m[0].length - 1 ];

        const yShape = LSTMMultivariateTimeSeries.getInputShape(y_matrix_m);
        return {
          yShape,
          xShape,
          y_matrix,
          x_matrix: x_matrix_timeseries,
        };
      }
      /**
       * @param {{layers:Array<Object>,compile:Object,fit:Object}} options - neural network configuration and tensorflow model hyperparameters
       * @param {{model:Object,tf:Object,}} properties - extra instance properties
       */
      constructor(options = {}, properties) {
        const config = Object.assign({
          layers: [],
          stateful: true,
          mulitpleTimeSteps:false,
          lookback: 1,
          features: undefined,
          outputs:1,
          learningRate: 0.1,
          compile: {
            loss: 'meanSquaredError',
            optimizer: 'adam',
          },
          fit: {
            epochs: 100,
            batchSize: 1,
            shuffle: false,
          },
        }, options);
        super(config, properties);
        this.createDataset = LSTMMultivariateTimeSeries.createDataset;
        this.seriesToSupervised = LSTMMultivariateTimeSeries.seriesToSupervised;
        this.getDropableColumns = LSTMMultivariateTimeSeries.getDropableColumns;
        this.drop = LSTMMultivariateTimeSeries.drop;
        this.getTimeseriesShape = LSTMMultivariateTimeSeries.getTimeseriesShape;
        this.getTimeseriesDataSet = LSTMMultivariateTimeSeries.getTimeseriesDataSet;
        return this;
      }
      /**
       * Adds dense layers to tensorflow classification model
       * @override
       * @param {Array<Array<number>>} x_matrix - independent variables
       * @param {Array<Array<number>>} y_matrix - dependent variables
       * @param {Array<Object>} layers - model dense layer parameters
       * @param {Array<Array<number>>} x_test - validation data independent variables
       * @param {Array<Array<number>>} y_test - validation data dependent variables
       */
      generateLayers(x_matrix, y_matrix, layers) {
        const xShape = this.getInputShape(x_matrix);
        const yShape = this.getInputShape(y_matrix);
        this.yShape = yShape;
        this.xShape = xShape;
        const lstmLayers = [];
        const denseLayers = [];
        if (layers) {
          if(layers.lstmLayers && layers.lstmLayers.length) lstmLayers.push(...layers.lstmLayers);
          if(layers.denseLayers && layers.denseLayers.length) denseLayers.push(...layers.denseLayers);

        } else {
          const inputShape = [ xShape[ 1 ], xShape[ 2 ], ];
          // console.log('default timeseries', { inputShape, xShape, yShape, });
          // model.add(LSTM(50, input_shape=(train_X.shape[1], train_X.shape[2])))
          // model.add(Dense(1))

          lstmLayers.push({ units: 10, inputShape,  });
          denseLayers.push({ units: yShape[1], });
        }
        // console.log('lstmLayers',lstmLayers)
        // console.log('denseLayers',denseLayers)
        if (lstmLayers.length) {
          lstmLayers.forEach(layer => {
            this.model.add(this.tf.layers.lstm(layer));
          });
        }
        if (denseLayers.length) {
          denseLayers.forEach(layer => {
            this.model.add(this.tf.layers.dense(layer));
          });
        }
        this.layers = {
          lstmLayers,
          denseLayers,
        };
        // this.settings.compile.optimizer = sgdoptimizer;
      }
      /**
       * @override
       * @param x_timeseries
       * @param y_timeseries
       * @param layers
       * @param x_test
       * @param y_test
       */
      async train(x_timeseries, y_timeseries, layers, x_test, y_test) {
        let xShape;
        let yShape;
        let x_matrix;
        let y_matrix;
        const look_back = this.settings.lookback;
        if (y_timeseries) {
          x_matrix = x_timeseries;
          y_matrix = y_timeseries;
          xShape = this.getInputShape(x_matrix);
          yShape = this.getInputShape(y_matrix);
        } else {
          const matrices = this.createDataset(x_timeseries, look_back);
          // console.log({matrices})
          x_matrix = matrices[ 0 ];
          y_matrix = this.reshape(matrices[ 1 ], [ matrices[ 1 ].length, 1 ]);
          xShape = this.getInputShape(x_matrix);
          yShape = this.getInputShape(y_matrix);
        }
        //_samples, _timeSteps, _features
        const timeseriesShape = this.getTimeseriesShape(x_matrix);
        // console.log({
        //   timeseriesShape, yShape, xShape,
        //   // y_matrix, x_matrix,
        // });
        const x_matrix_timeseries = LSTMMultivariateTimeSeries.reshape(x_matrix, timeseriesShape);
        // console.log('x_matrix_timeseries',JSON.stringify(x_matrix_timeseries));
        // console.log('x_matrix',JSON.stringify(x_matrix));
        // console.log('y_matrix',JSON.stringify(y_matrix));
        const xs = this.tf.tensor(x_matrix_timeseries, timeseriesShape);
        const ys = this.tf.tensor(y_matrix, yShape);
        this.xShape = timeseriesShape;
        this.yShape = yShape;
        this.model = this.tf.sequential();
        this.generateLayers.call(this, x_matrix_timeseries, y_matrix, layers || this.layers, x_test, y_test);
        this.model.compile(this.settings.compile);
        if (x_test && y_test) {
          this.settings.fit.validation_data = [ x_test, y_test, ];
        }
        await this.model.fit(xs, ys, this.settings.fit);
        // this.model.summary();
        xs.dispose();
        ys.dispose();
        return this.model;
      }
    }

    /**
     * Returns an array of vectors as an array of arrays (from modelscript)
     * @example
    const vectors = [ [1,2,3], [1,2,3], [3,3,4], [3,3,3] ];
    const arrays = pivotVector(vectors); // => [ [1,2,3,3], [2,2,3,3], [3,3,4,3] ];
     * @memberOf util
     * @param {Array[]} vectors
     * @returns {Array[]}
     * @ignore
     * @see {https://github.com/repetere/modelscript/blob/master/src/util.js}
     */
    /* istanbul ignore next */
    export function pivotVector(vectors=[]) {
      /* istanbul ignore next */
      return vectors.reduce((result, val, index/*, arr*/) => {
        val.forEach((vecVal, i) => {
          (index === 0)
            ? (result.push([vecVal,]))
            : (result[ i ].push(vecVal));
        });
        return result;
      }, []);
    }