import { TensorScriptOptions, TensorScriptProperties, Matrix, Vector, TensorScriptLayers, TensorScriptSavedLayers, NestedArray, TensorScriptModelContext,TensorScriptLSTMModelContext,  } from './model_interface';
import { LSTMTimeSeries, } from './lstm_time_series';
import range from 'lodash.range';

/**
 * Long Short Term Memory Multivariate Time Series with Tensorflow
 * @class LSTMMultivariateTimeSeries
 * @extends {LSTMTimeSeries}
 */
//@ts-ignore
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(this:TensorScriptModelContext, dataset:NestedArray<Array<number>> = [], look_back:number = 1):NestedArray<number> { 
    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;
    
    //@ts-ignore
    const series = LSTMMultivariateTimeSeries.seriesToSupervised(dataset, n_in, n_out);
    const dropped = LSTMMultivariateTimeSeries.getDropableColumns(features, n_in, n_out);
    //@ts-ignore
    const droppedColumns:any = 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, ]
    );
    //@ts-ignore
    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:NestedArray<Array<number>>, columns:Array<number>):NestedArray<Array<number>> {
    return data.reduce((cols:NestedArray<Array<number>>, row, i) => { 
      cols[ i ] = [];
      row.forEach((col: any, idx:number) => {
        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: number[], n_in:number = 1, n_out:number = 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: number[] = [];
    // let names = [];
    // input sequence (t-n, ... t-1)
    for (let x in data) {
      //@ts-ignore
      x = Number(parseInt(x));
      //@ts-ignore
      let maxIndex:number = x + n_in + n_out;
      if (maxIndex > data.length) break;
      //@ts-ignore
      cols[ x ] = [];
      // console.log({ x,maxIndex });
      for (let i in range(n_in)) {
        //@ts-ignore
        i = parseInt(i);
        //@ts-ignore
        cols[ x ].push(...data[x+i]);
        // console.log({ i, cols, });
      }
      for (let j in range(n_out)){
        //@ts-ignore
        j = parseInt(j);
        //@ts-ignore
        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: number, n_in: number, n_out: number) {
    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(this:TensorScriptModelContext, x_timeseries: NestedArray<any> | undefined) {
    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(this:TensorScriptModelContext, timeseries: NestedArray<number[]> | undefined, look_back: any) {
    const lookBack = look_back || this.settings.lookback;
    const matrices = LSTMMultivariateTimeSeries.createDataset.call(this, timeseries, lookBack);
    const x_matrix = matrices[0];
    //@ts-ignore
    const y_matrix_m = LSTMMultivariateTimeSeries.reshape(matrices[ 1 ], [ matrices[ 1 ].length, 1 ]);
    //@ts-ignore
    const timeseriesShape = LSTMMultivariateTimeSeries.getTimeseriesShape.call(this,x_matrix);
    // const timeseriesShape = LSTMMultivariateTimeSeries.getTimeseriesShape(x_matrix);
    //@ts-ignore
    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);
    //@ts-ignore
    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,
    };
  }
  createDataset: (...args: any[])=>NestedArray<number>;
  seriesToSupervised: (...args: any[])=>Array<number>;
  getDropableColumns: (...args: any[])=>any;
  drop: (...args: any[])=>any;
  getTimeseriesShape: (...args: any[])=>any;
  getTimeseriesDataSet: (...args: any[])=>any;
  /**
   * @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:TensorScriptOptions = {}, properties?:TensorScriptProperties) {
    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:Matrix, y_matrix:Matrix, layers: TensorScriptSavedLayers) {
    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(this:TensorScriptLSTMModelContext,x_timeseries: any, y_timeseries: any, layers:TensorScriptLayers, x_test:Matrix, y_test:Matrix) {
    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];
      //@ts-ignore
      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;
    if (this.trained===false) {
      this.model = this.tf.sequential();
      //@ts-ignore
      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 && this.settings.fit) {
      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:Matrix=[]):Matrix {
  /* istanbul ignore next */
  return vectors.reduce((result:Matrix, val, index/*, arr*/) => {
    val.forEach((vecVal, i) => {
      (index === 0)
        ? (result.push([vecVal,]))
        : (result[ i ].push(vecVal));
    });
    return result;
  }, []);
} 
