"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.asyncForEach = exports.flatten = exports.DimensionError = exports._reshape = exports.size = exports.TensorScriptModelInterface = exports.LambdaLayer = void 0;
// import * as tensorflow from '@tensorflow/tfjs';
// import '@tensorflow/tfjs-node';
// import * as tensorflow from '@tensorflow/tfjs-node';
const tf = __importStar(require("@tensorflow/tfjs-node"));
;
/******************************************************************************
 * tensorflow.js lambda layer
 * written by twitter.com/benjaminwegener
 * license: MIT
 * @see https://benjamin-wegener.blogspot.com/2020/02/tensorflowjs-lambda-layer.html
 */
class LambdaLayer extends tf.layers.Layer {
    constructor(config) {
        super(config);
        if (config.name === undefined) {
            config.name = ((+new Date) * Math.random()).toString(36); //random name from timestamp in case name hasn't been set
        }
        this.name = config.name;
        this.lambdaFunction = config.lambdaFunction;
        if (config.lambdaOutputShape)
            this.lambdaOutputShape = config.lambdaOutputShape;
    }
    call(input, kwargs) {
        // console.log({ input }, 'input[0].shape', input[0].shape)
        // input[0].data().then(inputData=>console.log)
        // console.log('input[0].data()', input[0].data())
        // return input;
        return tf.tidy(() => {
            // return tf.mean(tf.tensor(input),1,true)
            let result = new Array();
            // eval(this.lambdaFunction);
            result = (new Function('input', 'tf', this.lambdaFunction))(input, tf);
            // result = tf.mean(input,1);
            return result;
        });
    }
    computeOutputShape(inputShape) {
        // console.log('computeOutputShape',{inputShape})
        if (this.lambdaOutputShape === undefined) { //if no outputshape provided, try to set as inputshape
            return inputShape[0];
        }
        else {
            return this.lambdaOutputShape;
        }
    }
    getConfig() {
        const config = {
            ...super.getConfig(),
            lambdaFunction: this.lambdaFunction,
            lambdaOutputShape: this.lambdaOutputShape,
        };
        return config;
    }
    static get className() {
        return 'LambdaLayer';
    }
}
exports.LambdaLayer = LambdaLayer;
/**
 * 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
 */
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 = {}) {
        // tf.setBackend('cpu');
        this.type = 'ModelInterface';
        /** @type {Object} */
        this.settings = Object.assign({}, options);
        /** @type {Object} */
        this.model = properties.model;
        /** @type {Object} */
        this.tf = properties.tf || tf;
        /** @type {Boolean} */
        this.trained = false;
        this.compiled = false;
        /** @type {Function} */
        this.reshape = TensorScriptModelInterface.reshape;
        /** @type {Function} */
        this.getInputShape = TensorScriptModelInterface.getInputShape;
        if (this.tf && this.tf.serialization && this.tf.serialization.registerClass)
            this.tf.serialization.registerClass(LambdaLayer);
        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) {
            //@ts-ignore
            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), 
            //@ts-ignore
            product(size(array)), '!=');
        }
        try {
            newArray = _reshape(flatArray, shape);
        }
        catch (e) {
            if (e instanceof DimensionError) {
                throw new DimensionError(product(shape), 
                //@ts-ignore
                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 ,});
        //@ts-ignore
        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])
     */
    //@ts-ignore
    static getInputShape(matrix = []) {
        // static getInputShape(matrix:NestedArray<V>=[]):Shape {
        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;
    }
    exportConfiguration() {
        return {
            type: this.type,
            settings: this.settings,
            trained: this.trained,
            compiled: this.compiled,
            xShape: this.xShape,
            yShape: this.yShape,
            layers: this.layers,
        };
    }
    importConfiguration(configuration) {
        this.type = configuration.type || this.type;
        this.settings = {
            ...this.settings,
            ...configuration.settings,
        };
        this.trained = configuration.trained || this.trained;
        this.compiled = configuration.compiled || this.compiled;
        this.xShape = configuration.xShape || this.xShape;
        this.yShape = configuration.yShape || this.yShape;
        this.layers = configuration.layers || this.layers;
    }
    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, this is an alias for
     * @param {Object} options - tensorflow load model options
     * @return {Object} tensorflow model
     * @see {@link https://www.tensorflow.org/js/guide/save_load#loading_a_tfmodel}
     */
    async loadModel(options) {
        this.model = await this.tf.loadLayersModel(options);
        this.xShape = this.model.inputs[0].shape;
        this.yShape = this.model.outputs[0].shape;
        this.trained = true;
        return this.model;
    }
    /**
     * saves a tensorflow model, this is an alias for
     * @param {Object} options - tensorflow save model options
     * @return {Object} tensorflow model
     * @see {@link https://www.tensorflow.org/js/guide/save_load#save_a_tfmodel}
     */
    async saveModel(options) {
        const savedStatus = await this.model.save(options);
        return savedStatus;
    }
    /**
     * 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
     * @param {Boolean} [options.skip_matrix_check=false] - validate input is a matrix
     * @return {Array<number>|Array<Array<number>>} predicted model values
     */
    // async predict(options?:Matrix|Vector|InputTextArray|PredictionOptions):Promise<Matrix> 
    async predict(input_matrix, options) {
        if (!input_matrix || Array.isArray(input_matrix) === false)
            throw new Error('invalid input matrix');
        const config = {
            json: true,
            probability: true,
            ...options
        };
        const x_matrix = (Array.isArray(input_matrix) || config.skip_matrix_check)
            ? input_matrix
            : [
                input_matrix,
            ];
        return this.calculate(x_matrix)
            .data()
            .then((predictions) => {
            // console.log({ predictions });
            if (config.json === false) {
                return predictions;
            }
            else {
                if (!this.yShape)
                    throw new Error('Model is missing yShape');
                const shape = [x_matrix.length, this.yShape[1],];
                const predictionValues = (config.probability === false) ? Array.from(predictions).map(Math.round) : Array.from(predictions);
                return this.reshape(predictionValues, shape);
            }
        })
            .catch((e) => {
            throw e;
        });
    }
}
exports.TensorScriptModelInterface = TensorScriptModelInterface;
/**
 * 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 */
function size(x) {
    let s = [];
    while (Array.isArray(x)) {
        s.push(x.length);
        x = x[0];
    }
    return s;
}
exports.size = size;
/**
 * 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 */
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
        //@ts-ignore
        tmpArray = tmpArray2;
    }
    //@ts-ignore
    return tmpArray;
}
exports._reshape = _reshape;
/**
 * 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 */
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;
    }
}
exports.DimensionError = DimensionError;
/**
 * 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 */
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;
}
exports.flatten = flatten;
async function asyncForEach(array, callback) {
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array);
    }
}
exports.asyncForEach = asyncForEach;
