import { ComplexDecimal, TBinaryOperationName, TUnaryOperationLeftName } from './complex-decimal';
import { Evaluator, TNameTable } from './evaluator';

/**
 * # MultiArray
 *
 * A multimensional array library.
 */
export class MultiArray {
    /**
     * Dimensions property ([lines, columns, pages, blocks, ...]).
     */
    public dimension: number[];

    /**
     * Dimensions excluding columns getter ([lines, pages, blocks, ...]).
     */
    public get dimensionR(): number[] {
        return [this.dimension[0], ...this.dimension.slice(2)];
    }

    /**
     * Array property.
     */
    public array: ComplexDecimal[][];

    /**
     * Type property.
     */
    public type: number;

    /**
     * Parent node property.
     */
    public parent: any;

    /**
     * MultiArray constructor.
     * @param shape Array<number> of dimensions ([rows, columns, pages, blocks, ...]).
     * @param fill Data to fill MultiArray. The same object will be put in all elements of MultiArray.
     */
    public constructor(shape?: number[], fill?: any) {
        if (shape) {
            this.dimension = shape.slice();
            MultiArray.removeSingletonTail(this.dimension);
            this.array = new Array(this.dimensionR.reduce((p, c) => p * c, 1));
            if (fill) {
                for (let i = 0; i < this.array.length; i++) {
                    this.array[i] = new Array(this.dimension[1]).fill(fill);
                }
                this.type = fill.type;
            } else {
                for (let i = 0; i < this.array.length; i++) {
                    this.array[i] = new Array(this.dimension[1]).fill({ type: -1 });
                }
                this.type = -1;
            }
        } else {
            this.dimension = [0, 0];
            this.array = [];
            this.type = -1;
        }
    }

    /**
     * Check if object is a MultiArray.
     * @param obj Any object.
     * @returns true if object is a MultiArray. false otherwise.
     */
    public static isThis(obj: any): boolean {
        return 'array' in obj;
    }

    /**
     * Check if object is a MultiArray and it is a row vector. To be used
     * only in Evaluator to test multiple assignment. The restrictions
     * imposed by parser can restrict the check only to `obj.dimension[0] === 1`.
     * @param obj
     * @returns true if object is a row vector. false otherwise.
     */
    public static isRowVector(obj: any): boolean {
        return 'array' in obj && obj.dimension[0] === 1;
    }

    /**
     * Set type property in place with maximum value of array items type.
     * @param M MultiArray to set type property.
     */
    public static setType(M: MultiArray): void {
        M.type = Math.max(...M.array.map((row) => Math.max(...row.map((value) => value.type))));
    }

    /**
     * Test if two array are equals.
     * @param left Array<boolean | number | string>.
     * @param right Array<boolean | number | string>.
     * @returns true if two arrays are equals. false otherwise.
     */
    public static arrayEquals(a: (boolean | number | string)[], b: (boolean | number | string)[]): boolean {
        return a.length === b.length && a.every((value, index) => value === b[index]);
    }

    /**
     * Returns a one-based range array ([1, 2, ..., length]).
     * @param length Length or last value of range array.
     * @returns Range array.
     */
    public static rangeArray(length: number): number[] {
        const result = [];
        for (let i = 1; i <= length; i++) {
            result.push(i);
        }
        return result;
    }

    /**
     * Converts linear index to subscript.
     * @param dimension Dimensions of multidimensional array ([line, column, page, block, ...]).
     * @param index Zero-based linear index.
     * @returns One-based subscript ([line, column, page, block, ...]).
     */
    public static linearIndexToSubscript(dimension: number[], index: number): number[] {
        return dimension.map((dim, i) => (Math.floor(index / dimension.slice(0, i).reduce((p, c) => p * c, 1)) % dim) + 1);
    }

    /**
     * Converts subscript to linear index.
     * @param dimension Dimensions of multidimensional array ([line, column, page, block, ...]).
     * @param subscript One-based subscript ([line, column, page, block, ...]).
     * @returns Zero-based linear index.
     */
    public static subscriptToLinearIndex(dimension: number[], subscript: number[]): number {
        return subscript.reduce((p, c, i) => p + (c - 1) * dimension.slice(0, i).reduce((p, c) => p * c, 1), 0);
    }

    /**
     * Converts linear index to Multiarray.array subscript.
     * @param row Row dimension.
     * @param column Column dimension.
     * @param index Zero-based linear index.
     * @returns Multiarray.array subscript ([row, column]).
     */
    public static linearIndexToMultiArrayRowColumn(row: number, column: number, index: number): [number, number] {
        const pageLength = row * column;
        const indexPage = index % pageLength;
        return [Math.floor(index / pageLength) * row + (indexPage % row), Math.floor(indexPage / row)];
    }

    /**
     * Converts MultiArray subscript to Multiarray.array subscript.
     * @param dimension MultiArray dimension.
     * @param subscript Subscript.
     * @returns Multiarray.array subscript ([row, column]).
     */
    public static subscriptToMultiArrayRowColumn(dimension: number[], subscript: number[]): [number, number] {
        const index = subscript.reduce((p, c, i) => p + (c - 1) * dimension.slice(0, i).reduce((p, c) => p * c, 1), 0);
        const pageLength = dimension[0] * dimension[1];
        const indexPage = index % pageLength;
        return [Math.floor(index / pageLength) * dimension[0] + (indexPage % dimension[0]), Math.floor(indexPage / dimension[0])];
    }

    /**
     * Base method of the ind2sub function. Returns dimension.length + 1
     * dimensions. If the index exceeds the dimensions, the last dimension
     * will contain the multiplier of the other dimensions. Otherwise it will
     * be 1.
     * @param dimension Array of dimensions.
     * @param index One-base linear index.
     * @returns One-based subscript ([line, column, page, block, ...]).
     */
    public static ind2subNumber(dimension: number[], index: number): number[] {
        dimension = [...dimension, index + 1];
        return dimension.map((dim, i) => Math.floor((index - 1) / dimension.slice(0, i).reduce((p, c) => p * c, 1)) % dim).map((d) => d + 1);
    }

    /**
     * Returns the number of elements in M.
     * @param M Multidimensional array.
     * @returns Number of elements in M.
     */
    public static linearLength(M: MultiArray): number {
        return M.array.length * M.dimension[1];
    }

    /**
     * Get dimension at index d of MultiArray M
     * @param M Multiarray.
     * @param d Zero-based dimension index.
     * @returns Dimension d.
     */
    public static getDimension(M: MultiArray, d: number): number {
        return d < M.dimension.length ? M.dimension[d] : 1;
    }

    /**
     * Remove singleton tail of dimension array in place.
     * @param dimension Dimension array.
     */
    public static removeSingletonTail(dimension: number[]): void {
        let i = dimension.length - 1;
        while (dimension[i] === 1 && i > 1) {
            dimension.pop();
            i--;
        }
    }

    /**
     * Append singleton tail of dimension array in place.
     * @param dimension Dimension array.
     * @param length Resulting length of dimension array.
     */
    public static appendSingletonTail(dimension: number[], length: number): void {
        if (length > dimension.length) {
            dimension.push(...new Array(length - dimension.length).fill(1));
        }
    }

    /**
     * Find first non-single dimension.
     * @param M MultiArray.
     * @returns First non-single dimension of `M`.
     */
    public static firstNonSingleDimension(M: MultiArray): number {
        for (let i = 0; i < M.dimension.length; i++) {
            if (M.dimension[i] !== 1) {
                return i;
            }
        }
        return M.dimension.length - 1;
    }

    /**
     * Creates a MultiArray object from the first row of elements (for
     * parsing purposes).
     * @param row Array of objects.
     * @returns MultiArray with `row` parameter as first line.
     */
    public static firstRow(row: any[]): MultiArray {
        const result = new MultiArray([1, row.length]);
        result.array[0] = row;
        return result;
    }

    /**
     * Append a row of elements to a MultiArray object (for parsing
     * purposes).
     * @param M MultiArray.
     * @param row Array of objects to append as row of MultiArray.
     * @returns MultiArray with row appended.
     */
    public static appendRow(M: MultiArray, row: any[]): MultiArray {
        M.array.push(row);
        M.dimension[0]++;
        return M;
    }

    /**
     * Swap two rows of a MultiArray in place.
     * @param M
     * @param m
     * @param n
     */
    public static swapRows(M: MultiArray, m: number, n: number): void {
        const row = M.array[m];
        M.array[m] = M.array[n];
        M.array[n] = row;
    }

    /**
     * Unparse MultiArray.
     * @param M MultiArray object.
     * @returns String of unparsed MultiArray.
     */
    public static unparse(M: MultiArray): string {
        const unparseRows = (row: any[]) => row.map((value) => global.EvaluatorPointer.Unparse(value)).join() + ';\n';
        let arraystr: string = '';
        if (M.dimension.length > 2) {
            let result = '';
            for (let p = 0; p < M.array.length; p += M.dimension[0]) {
                arraystr = M.array
                    .slice(p, p + M.dimension[0])
                    .map(unparseRows)
                    .join('');
                arraystr = arraystr.substring(0, arraystr.length - 2);
                result += `[\n${arraystr}\n] (:,:,${MultiArray.linearIndexToSubscript(M.dimensionR, p).slice(1).join()})\n`;
            }
            return result;
        } else {
            arraystr = M.array.map(unparseRows).join('');
            arraystr = arraystr.substring(0, arraystr.length - 2);
            return `[\n${arraystr}\n]`;
        }
    }

    /**
     * Unparse MultiArray as MathML language.
     * @param M MultiArray object.
     * @returns String of unparsed MultiArray in MathML language.
     */
    public static unparseMathML(M: MultiArray): string {
        const unparseRows = (row: any[]) => `<mtr>${row.map((value) => `<mtd>${global.EvaluatorPointer.unparserMathML(value)}</mtd>`).join('')}</mtr>`;
        const buildMrow = (rows: string) => `<mrow><mo>[</mo><mtable>${rows}</mtable><mo>]</mo></mrow>`;
        if (M.dimension[0] === 0 && M.dimension[1] === 0) {
            return '<mrow><mo>[</mo><mtable><mspace width="0.5em"/></mtable><mo>]</mo></mrow><mo>(</mo><mn>0</mn><mi>&times;</mi><mn>0</mn><mo>)</mo>';
        }
        if (M.dimension.length > 2) {
            let result = '';
            for (let p = 0; p < M.array.length; p += M.dimension[0]) {
                const array = M.array
                    .slice(p, p + M.dimension[0])
                    .map(unparseRows)
                    .join('');
                const subscript = MultiArray.linearIndexToSubscript(M.dimensionR, p)
                    .slice(1)
                    .map((d) => `<mn>${d}</mn>`)
                    .join('<mo>,</mo>');
                result += `<mtr><mtd><msub>${buildMrow(array)}<mrow><mo>(</mo><mo>:</mo><mo>,</mo><mo>:</mo><mo>,</mo>${subscript}<mo>)</mo></mrow></msub></mtd></mtr>`;
            }
            return `<mtable>${result}</mtable>`;
        } else {
            return buildMrow(M.array.map(unparseRows).join(''));
        }
    }

    /**
     * Evaluate array. Calls `global.EvaluatorPointer.Evaluator` function for each element of page (matrix row-ordered)
     * @param array Matrix.
     * @param local `local` Evaluator parameter.
     * @param fname `fname` Evaluator parameter.
     * @param parent Parent node of items in page.
     * @returns Evaluated matrix.
     */
    private static evaluatePage(array: any[][], local: boolean = false, fname: string = '', parent: any): any[][] {
        const result: any[][] = [];
        for (let i = 0, k = 0; i < array.length; i++, k++) {
            result.push([]);
            let h = 1;
            for (let j = 0; j < array[i].length; j++) {
                array[i][j].parent = parent;
                const element = global.EvaluatorPointer.Evaluator(array[i][j], local, fname);
                if ('array' in element) {
                    if (j === 0) {
                        h = element.array.length;
                        result.splice(k, 1, element.array[0]);
                        for (let n = 1; n < h; n++) {
                            result.splice(k + n, 0, element.array[n]);
                        }
                    } else {
                        for (let n = 0; n < element.array.length; n++) {
                            result[k + n].push(...element.array[n]);
                        }
                    }
                } else {
                    result[k][j] = element;
                }
            }
            k += h - 1;
            if (i != 0) {
                if (result[i].length != result[0].length) {
                    throw new EvalError(`vertical dimensions mismatch (${k}x${result[0].length} vs 1x${result[i].length}).`);
                }
            }
        }
        return result;
    }

    /**
     * Evaluate MultiArray object. Calls `MultiArray.evaluatePage` method for each page of
     * multidimensional array.
     * @param M MultiArray object.
     * @param local Local context (function evaluation).
     * @param fname Function name (context).
     * @returns Evaluated MultiArray object.
     */
    public static evaluate(M: MultiArray, local: boolean = false, fname: string = ''): MultiArray {
        const result: MultiArray = new MultiArray();
        for (let p = 0; p < M.array.length; p += M.dimension[0]) {
            const page = MultiArray.evaluatePage(M.array.slice(p, p + M.dimension[0]), local, fname, result);
            if (p === 0) {
                result.dimension = [page.length, page[0].length, ...M.dimension.slice(2)];
            } else {
                if (result.dimension[0] !== page.length || result.dimension[1] !== page[0].length) {
                    throw new EvalError(`page dimensions mismatch (${result.dimension[0]}x${result.dimension[1]} vs ${page.length}x${page[0].length}).`);
                }
            }
            for (let i = 0; i < page.length; i++) {
                result.array[p + i] = page[i];
            }
        }
        MultiArray.setType(result);
        return result;
    }

    /**
     * Linearize MultiArray in an array of any using column-major
     * order.
     * @param M Multidimensional array.
     * @returns `any[]` of multidimensional array `M` linearized.
     */
    public static linearize(M: MultiArray | any): any[] {
        if ('array' in M) {
            const result: any[] = [];
            for (let p = 0; p < M.array.length; p += M.dimension[0]) {
                for (let j = 0; j < M.dimension[1]; j++) {
                    result.push(...M.array.slice(p, p + M.dimension[0]).map((row: any[]) => row[j]));
                }
            }
            return result;
        } else {
            return [M];
        }
    }

    /**
     * Returns a null array (0x0 matrix).
     * @returns Null array (0x0 matrix).
     */
    public static array_0x0(): MultiArray {
        return new MultiArray([0, 0]);
    }

    /**
     * Convert a scalar value to 1x1 MultiArray.
     * @param value MultiArray or scalar.
     * @returns MultiArray 1x1 if value is scalar.
     */
    public static scalarToMultiArray(value: MultiArray | any): MultiArray {
        if ('array' in value) {
            return value;
        } else {
            const result = new MultiArray([1, 1]);
            result.array[0] = [value];
            result.type = value.type;
            return result;
        }
    }

    /**
     * If `value` parameter is a MultiArray of size 1x1 then returns as scalar.
     * @param value MultiArray or scalar.
     * @returns Scalar value if `value` parameter has all dimensions as singular.
     */
    public static MultiArrayToScalar(value: MultiArray | any): MultiArray | any {
        if ('array' in value && value.dimension.length === 2 && value.dimension[0] === 1 && value.dimension[1] === 1) {
            return value.array[0][0];
        } else {
            return value;
        }
    }

    /**
     * If `value` parameter is a MultiArray returns it's first element.
     * Otherwise returns `value` parameter.
     * @param value
     * @returns
     */
    public static firstElement(value: MultiArray | any): any {
        if ('array' in value) {
            if (value.dimension.reduce((p: number, c: number) => p * c, 1) > 0) {
                return value.array[0][0];
            } else {
                throw new Error('Cannot get first element of array. Array is [](0x0).');
            }
        } else {
            return value;
        }
    }

    /**
     * Converts a ComplexDecimal array or a single line MultiArray to an array
     * or number.
     * @param M
     * @returns
     */
    public static oneRowToDim(M: ComplexDecimal[] | MultiArray): number[] {
        if (Array.isArray(M)) {
            return M.map((data) => data.re.toNumber());
        } else {
            return M.array[0].map((data) => data.re.toNumber());
        }
    }

    /**
     * Create MultiArray with all elements equals `fill` parameter.
     * @param fill Value to fill MultiArray.
     * @param dimension Dimensions of created MultiArray.
     * @returns MultiArray filled with `fill` parameter.
     */
    public static newFilled(fill: any, ...dimension: any): MultiArray | ComplexDecimal {
        if (dimension.length === 0) {
            return fill;
        } else if (dimension.length === 1) {
            if ('array' in dimension[0]) {
                return MultiArray.MultiArrayToScalar(new MultiArray(MultiArray.oneRowToDim(dimension[0]), fill));
            } else {
                return MultiArray.MultiArrayToScalar(new MultiArray([dimension[0].re.toNumber(), dimension[0].re.toNumber()], fill));
            }
        } else {
            return MultiArray.MultiArrayToScalar(new MultiArray(MultiArray.oneRowToDim(dimension), fill));
        }
    }

    /**
     * Create MultiArray with all elements filled with `fillFunction` result.
     * The parameter passed to `fillFunction` is a linear index of element.
     * @param fillFunction Function to be called and the result fills element of MultiArray created.
     * @param dimension Dimensions of created MultiArray.
     * @returns MultiArray filled with `fillFunction` results for each element.
     */
    public static newFilledEach(fillFunction: (index: number) => any, ...dimension: any): MultiArray | ComplexDecimal {
        let result: MultiArray;
        if (dimension.length === 0) {
            return fillFunction(0);
        } else if (dimension.length === 1) {
            if ('array' in dimension[0]) {
                result = new MultiArray(MultiArray.oneRowToDim(dimension[0]));
            } else {
                result = new MultiArray([dimension[0].re.toNumber(), dimension[0].re.toNumber()]);
            }
        } else {
            result = new MultiArray(MultiArray.oneRowToDim(dimension));
        }
        for (let n = 0; n < MultiArray.linearLength(result); n++) {
            const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(result.dimension[0], result.dimension[1], n);
            result.array[i][j] = fillFunction(n);
        }
        MultiArray.setType(result);
        return MultiArray.MultiArrayToScalar(result);
    }

    /**
     * Copy of MultiArray.
     * @param M MultiArray.
     * @returns Copy of MultiArray.
     */
    public static copy(M: MultiArray): MultiArray {
        const result = new MultiArray(M.dimension);
        result.array = M.array.map((row) => row.map((value) => (ComplexDecimal.isThis(value) ? ComplexDecimal.copy(value) : Object.assign({}, value))));
        result.type = M.type;
        return result;
    }

    /**
     * Convert MultiArray to logical value. It's true if all elements is
     * non-null. Otherwise is false.
     * @param M
     * @returns
     */
    public static toLogical(M: MultiArray): ComplexDecimal {
        for (let i = 0; i < M.array.length; i++) {
            const row = M.array[i];
            for (let j = 0; j < M.dimension[1]; j++) {
                const value = ComplexDecimal.toMaxPrecision(row[j]);
                if (value.re.eq(0) && value.im.eq(0)) {
                    return ComplexDecimal.false();
                }
            }
        }
        return ComplexDecimal.true();
    }

    /**
     * Calls a defined callback function on each element of an MultiArray,
     * and returns an MultiArray that contains the results.
     * @param M Matrix.
     * @param f Function mapping.
     * @returns
     */
    public static map(M: MultiArray, f: Function): MultiArray {
        const result = new MultiArray(M.dimension);
        result.array = M.array.map((row) => row.map(f as any));
        MultiArray.setType(result);
        return result;
    }

    /**
     * Expand Multidimensional array dimensions if dimensions in `dim` is greater than dimensions of `M`.
     * If a dimension of `M` is greater than corresponding dimension in `dim` it's unchanged.
     * The array is filled with zeros and is expanded in place.
     * @param M Multidimensional array.
     * @param dim New dimensions.
     */
    public static expand(M: MultiArray, dim: number[]): void {
        let dimM = M.dimension.slice();
        let dimension = dim.slice();
        if (dimM.length < dimension.length) {
            dimM = dimM.concat(new Array(dimension.length - dimM.length).fill(1));
        }
        if (dimension.length < dimM.length) {
            dimension = dimension.concat(new Array(dimM.length - dimension.length).fill(1));
        }
        const resultDimension = dimension.map((d, i) => Math.max(d, dimM[i]));
        if (MultiArray.arrayEquals(dimM, resultDimension)) {
            return;
        }
        const result = new MultiArray(resultDimension, ComplexDecimal.zero());
        for (let n = 0; n < MultiArray.linearLength(M); n++) {
            const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(M.dimension[0], M.dimension[1], n);
            const subscriptM = MultiArray.linearIndexToSubscript(M.dimension, n);
            const [p, q] = MultiArray.subscriptToMultiArrayRowColumn(result.dimension, subscriptM);
            result.array[p][q] = M.array[i][j];
        }
        MultiArray.removeSingletonTail(result.dimension);
        M.dimension = result.dimension;
        M.array = result.array;
    }

    /**
     * Expand range.
     * @param startNode Start of range.
     * @param stopNode Stop of range.
     * @param strideNode Optional stride value.
     * @returns MultiArray of range expanded.
     */
    public static expandRange(startNode: ComplexDecimal, stopNode: ComplexDecimal, strideNode?: ComplexDecimal | null): MultiArray {
        const expanded = [];
        const s = strideNode ? strideNode.re.toNumber() : 1;
        for (let n = startNode.re.toNumber(), i = 0; s > 0 ? n <= stopNode.re.toNumber() : n >= stopNode.re.toNumber(); n += s, i++) {
            expanded[i] = new ComplexDecimal(n);
        }
        const result = new MultiArray([1, expanded.length]);
        result.array = [expanded];
        MultiArray.setType(result);
        return result;
    }

    /**
     * Check if subscript is a integer number, convert ComplexDecimal to
     * number.
     * @param k Index as ComplexDecimal.
     * @param input Optional id reference of object.
     * @returns k as number, if real part is integer greater than 1 and imaginary part is 0.
     */
    public static testIndex(k: ComplexDecimal, input?: string): number {
        if (!k.re.isInteger() || k.re.lt(1)) {
            throw new RangeError(`${input ? `${input}: ` : ``}subscripts must be either integers greater than or equal 1 or logicals.`);
        }
        if (!k.im.eq(0)) {
            throw new RangeError(`${input ? `${input}: ` : ``}subscripts must be real.`);
        }
        return k.re.toNumber();
    }

    /**
     * Check if subscript is a integer number, convert ComplexDecimal to
     * number, then check if it's less than bound.
     * @param k Index as ComplexDecimal.
     * @param bound Maximum acceptable value for the index
     * @param dim Dimensions (to generate error message)
     * @param input Optional string to generate error message.
     * @returns Index as number.
     */
    public static testIndexBound(k: ComplexDecimal, bound: number, dim: number[], input?: string): number {
        const result = MultiArray.testIndex(k, input);
        if (result > bound) {
            throw new RangeError(`${input ? `${input}: ` : ``}out of bound ${bound} (dimensions are ${dim.join('x')}).`);
        }
        return result;
    }

    /**
     * Converts subscript to linear index. Performs checks and throws
     * comprehensive errors if dimension bounds are exceeded.
     * @param dimension Dimension of multidimensional array ([line, column, page, block, ...]) as number[].
     * @param subscript Subscript ([line, column, page, block, ...]) as a ComplexDecimal[].
     * @param input Input string to generate error messages (the id of array).
     * @returns linear index.
     */
    public static parseSubscript(dimension: number[], subscript: ComplexDecimal[], input?: string, that?: Evaluator): number {
        // Converts ComplexDecimal[] subscript parameter to number[].
        const index = subscript.map((i) => MultiArray.testIndex(i, `${input ? input : ''}${that ? '(' + subscript.map((i) => that.Unparse(i)).join() + ')' : ''}`));
        /**
         * Throws comprehensive out of bound error indicating subscript index and bound.
         * @param indexPosition Position of subscript index out of bound.
         * @param bound Bound.
         */
        const throwError = (indexPosition: number, bound: number): void => {
            /**
             * Create notation to denote irrelevant subscripts. Returns `'_,_,_,_'`
             * with `length` `'_'` elements or `'...[x${length}]...'` if length > 4.
             * @param length Length of notation.
             * @returns String notation.
             */
            const irrelevantSubscript = (length: number): string => {
                return length > 4 ? `...[x${length}]...` : new Array(length).fill('_').join();
            };
            const left = irrelevantSubscript(indexPosition);
            const right = irrelevantSubscript(index.length - indexPosition - 1);
            throw new RangeError(
                `${input ? input : ''}(${left}${!!left ? ',' : ''}${index[indexPosition]}${!!right ? ',' : ''}${right}): out of bound ${bound} (dimensions are ${dimension.join(
                    'x',
                )}).`,
            );
        };
        // Copy index to indexReduced and remove singleton tail.
        const indexReduced = index.slice();
        MultiArray.removeSingletonTail(indexReduced);
        if (indexReduced.length > dimension.length) {
            // Error if indexReduced has more dimensions than dimension parameter.
            const test = index.map((i, n) => i > dimension[n]);
            const dimFail = test.indexOf(true);
            if (dimFail >= 0) {
                throwError(dimFail, 1);
            }
        }
        let dim: number[];
        if (index.length < dimension.length) {
            // Copy dimension parameter.
            dim = dimension.slice();
            // Test if some index greater than dim.
            const test = index.map((i, n) => i > dimension[n]);
            const dimFail = test.indexOf(true);
            if (dimFail >= 0) {
                if (dimFail === index.length - 1) {
                    // Last index is greater than corresponding dimension. Test if it's greater than dimension tail.
                    const bound = dim.slice(index.length - 1).reduce((p, c) => p * c, 1);
                    if (index[index.length - 1] > bound) {
                        throwError(dimFail, bound);
                    }
                } else {
                    // Error before last index.
                    throwError(dimFail, dim[dimFail]);
                }
            }
        } else {
            // Copy dimension parameter and append 1 until it has the same length of index if necessary.
            dim = dimension.concat(new Array(index.length - dimension.length).fill(1));
            // Test if some index greater than dim.
            const test = index.map((i, n) => i > dimension[n]);
            const dimFail = test.indexOf(true);
            if (dimFail >= 0) {
                throwError(dimFail, dim[dimFail]);
            }
        }
        return indexReduced.reduce((p, c, i) => p + (c - 1) * dimension.slice(0, i).reduce((p, c) => p * c, 1), 0);
    }

    /**
     * Binary operation 'scalar `operation` array'.
     * @param op Binary operation name.
     * @param left Left operand (scalar).
     * @param right Right operand (array).
     * @returns Result of operation.
     */
    public static scalarOpMultiArray(op: TBinaryOperationName, left: ComplexDecimal, right: MultiArray): MultiArray {
        const result = new MultiArray(right.dimension);
        result.array = right.array.map((row) => row.map((value) => ComplexDecimal[op](left, value)));
        MultiArray.setType(result);
        return result;
    }

    /**
     * Binary operation 'array `operation` scalar'.
     * @param op Binary operation name.
     * @param left Left operand (array).
     * @param right Right operaand (scalar).
     * @returns Result of operation.
     */
    public static MultiArrayOpScalar(op: TBinaryOperationName, left: MultiArray, right: ComplexDecimal): MultiArray {
        const result = new MultiArray(left.dimension);
        result.array = left.array.map((row) => row.map((value) => ComplexDecimal[op](value, right)));
        MultiArray.setType(result);
        return result;
    }

    /**
     * Unary left operation.
     * @param op Unary operation name.
     * @param right Operand (array)
     * @returns Result of operation.
     */
    public static leftOperation(op: TUnaryOperationLeftName, right: MultiArray): MultiArray {
        const result = new MultiArray(right.dimension);
        result.array = right.array.map((row) => row.map((value) => ComplexDecimal[op](value)));
        MultiArray.setType(result);
        return result;
    }

    /**
     * Binary element-wise operatior.
     * @param op Binary operatior.
     * @param left Left operand.
     * @param right Right operand.
     * @returns Binary element-wise result.
     */
    public static elementWiseOperation(op: TBinaryOperationName, left: MultiArray, right: MultiArray): MultiArray {
        let leftDimension = left.dimension.slice();
        let rightDimension = right.dimension.slice();
        if (leftDimension.length < rightDimension.length) {
            leftDimension = leftDimension.concat(new Array(rightDimension.length - leftDimension.length).fill(1));
        }
        if (rightDimension.length < leftDimension.length) {
            rightDimension = rightDimension.concat(new Array(leftDimension.length - rightDimension.length).fill(1));
        }
        if (MultiArray.arrayEquals(leftDimension, rightDimension)) {
            // No broadcasting.
            const result = new MultiArray(leftDimension);
            result.array = left.array.map((row, i) => row.map((value, j) => ComplexDecimal[op](value, right.array[i][j])));
            MultiArray.setType(result);
            return result;
        } else {
            // Broadcasting
            const leftBroadcast = new Array(leftDimension.length);
            const rightBroadcast = new Array(rightDimension.length);
            const resultDimension = new Array(leftDimension.length);
            for (let d = 0; d < leftDimension.length; d++) {
                if (leftDimension[d] === rightDimension[d]) {
                    leftBroadcast[d] = false;
                    rightBroadcast[d] = false;
                    resultDimension[d] = leftDimension[d];
                } else if (leftDimension[d] === 1) {
                    leftBroadcast[d] = true;
                    rightBroadcast[d] = false;
                    resultDimension[d] = rightDimension[d];
                } else if (rightDimension[d] === 1) {
                    leftBroadcast[d] = false;
                    rightBroadcast[d] = true;
                    resultDimension[d] = leftDimension[d];
                } else {
                    throw new EvalError(`operator ${op}: nonconformant arguments (op1 is ${left.dimension.join('x')}, op2 is ${right.dimension.join('x')}).`);
                }
            }
            const result = new MultiArray(resultDimension);
            const resultLinearLength = MultiArray.subscriptToLinearIndex(resultDimension, resultDimension) + 1;
            for (let n = 0; n < resultLinearLength; n++) {
                const resultSubscript = MultiArray.linearIndexToSubscript(resultDimension, n);
                const leftSubscript = resultSubscript.map((s, i) => (leftBroadcast[i] ? 1 : s));
                const leftLinear = MultiArray.subscriptToLinearIndex(leftDimension, leftSubscript);
                const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(leftDimension[0], leftDimension[1], leftLinear);
                const rightSubscript = resultSubscript.map((s, i) => (rightBroadcast[i] ? 1 : s));
                const rightLinear = MultiArray.subscriptToLinearIndex(rightDimension, rightSubscript);
                const [k, l] = MultiArray.linearIndexToMultiArrayRowColumn(rightDimension[0], rightDimension[1], rightLinear);
                const [o, p] = MultiArray.linearIndexToMultiArrayRowColumn(resultDimension[0], resultDimension[1], n);
                result.array[o][p] = ComplexDecimal[op](left.array[i][j], right.array[k][l]);
            }
            MultiArray.setType(result);
            return result;
        }
    }

    /**
     * Reduce one dimension of MultiArray putting entire dimension in one
     * element of resulting MultiArray as an Array. The resulting MultiArray
     * cannot be unparsed or used as argument of any other method of
     * MultiArray class.
     * @param dimension Dimension to reduce to Array
     * @param M MultiArray to be reduced.
     * @returns MultiArray reduced.
     */
    public static reduceToArray(dimension: number, M: MultiArray): MultiArray {
        if (dimension >= M.dimension.length) {
            return M;
        } else {
            const dimResult = M.dimension.slice();
            dimResult[dimension] = 1;
            const result = new MultiArray(dimResult);
            const subscriptC = M.dimension.slice();
            subscriptC[dimension] = 1;
            const length = subscriptC.reduce((p, c) => p * c, 1);
            for (let d = 1; d <= M.dimension[dimension]; d++) {
                const subscriptC = M.dimension.slice();
                subscriptC[dimension] = 1;
                const args = subscriptC.map((s) => MultiArray.rangeArray(s));
                args[dimension] = [d];
                for (let n = 0; n < length; n++) {
                    const subscriptM = MultiArray.linearIndexToSubscript(subscriptC, n).map((s, r) => args[r][s - 1]);
                    const linearM = MultiArray.subscriptToLinearIndex(M.dimension, subscriptM);
                    const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(M.dimension[0], M.dimension[1], linearM);
                    const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(result.dimension[0], result.dimension[1], n);
                    if (d === 1) {
                        result.array[p][q] = [M.array[i][j]] as unknown as ComplexDecimal;
                    } else {
                        (result.array[p][q] as unknown as ComplexDecimal[]).push(M.array[i][j]);
                    }
                }
            }
            result.type = M.type;
            return result;
        }
    }

    /**
     * Contract MultiArray along `dimension` calling callback. This method is
     * analogous to the JavaScript Array.reduce function.
     * @param dimension Dimension to operate callback and contract.
     * @param M Multidimensional array.
     * @param callback Reduce function.
     * @param initial Optional initial value to set as previous in the first
     * call of callback. If not set the previous will be set to the first
     * element of dimension.
     * @returns Multiarray with `dimension` reduced using `callback`.
     */
    public static reduce(dimension: number, M: MultiArray, callback: (previous: any, current: any, index?: number) => any, initial?: any): MultiArray | ComplexDecimal {
        if (dimension >= M.dimension.length) {
            return M;
        } else {
            const dimResult = M.dimension.slice();
            dimResult[dimension] = 1;
            const result = new MultiArray(dimResult);
            const subscriptC = M.dimension.slice();
            subscriptC[dimension] = 1;
            const length = subscriptC.reduce((p, c) => p * c, 1);
            const args = subscriptC.map((s) => MultiArray.rangeArray(s));
            for (let n = 0; n < length; n++) {
                const subscriptM = MultiArray.linearIndexToSubscript(subscriptC, n).map((s, r) => args[r][s - 1]);
                const linearM = MultiArray.subscriptToLinearIndex(M.dimension, subscriptM);
                const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(M.dimension[0], M.dimension[1], linearM);
                const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(result.dimension[0], result.dimension[1], n);
                result.array[p][q] = initial ? callback(initial, M.array[i][j], n) : M.array[i][j];
            }
            for (let d = 2; d <= M.dimension[dimension]; d++) {
                const subscriptC = M.dimension.slice();
                subscriptC[dimension] = 1;
                const args = subscriptC.map((s) => MultiArray.rangeArray(s));
                args[dimension] = [d];
                for (let n = 0; n < length; n++) {
                    const subscriptM = MultiArray.linearIndexToSubscript(subscriptC, n).map((s, r) => args[r][s - 1]);
                    const linearM = MultiArray.subscriptToLinearIndex(M.dimension, subscriptM);
                    const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(M.dimension[0], M.dimension[1], linearM);
                    const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(result.dimension[0], result.dimension[1], n);
                    const subscriptP = MultiArray.linearIndexToSubscript(result.dimension, n);
                    subscriptP[dimension] = 1;
                    const [r, s] = MultiArray.subscriptToMultiArrayRowColumn(result.dimension, subscriptP);
                    result.array[p][q] = callback(result.array[r][s], M.array[i][j], n);
                }
            }
            MultiArray.setType(result);
            return MultiArray.MultiArrayToScalar(result);
        }
    }

    /**
     * Return the concatenation of N-D array objects, ARRAY1, ARRAY2, ...,
     * ARRAYN along `dimension` parameter (zero-based).
     * @param dimension Dimension of concatenation.
     * @param fname Function name (for error messages).
     * @param ARRAY Arrays to concatenate.
     * @returns Concatenated arrays along `dimension` parameter.
     */
    public static concatenate(dimension: number, fname: string, ...ARRAY: MultiArray[]): MultiArray {
        // Get all ARRAY dimension and set 0 at dimension[dimension]
        const catDims: number[] = [];
        const dims = ARRAY.map((array) => {
            const dim = array.dimension.slice();
            MultiArray.appendSingletonTail(dim, dimension + 1);
            catDims.push(dim[dimension]);
            dim[dimension] = 0;
            return dim;
        });
        // Check if all ARRAY dimensions are equals except for dimension parameter.
        if (!dims.every((dim) => MultiArray.arrayEquals(dim, dims[0]))) {
            throw new EvalError(`${fname}: dimension mismatch`);
        }
        const resultDim = dims[0].slice();
        resultDim[dimension] = catDims.reduce((p, c) => p + c, 0);
        const result = new MultiArray(resultDim);
        ARRAY.forEach((array, a) => {
            const shift = catDims.slice(0, a).reduce((p, c) => p + c, 0);
            for (let n = 0; n < MultiArray.linearLength(array); n++) {
                const arrayDim = array.dimension.slice();
                MultiArray.appendSingletonTail(arrayDim, dimension + 1);
                const subscript = MultiArray.linearIndexToSubscript(arrayDim, n);
                subscript[dimension] += shift;
                const [i, j] = MultiArray.subscriptToMultiArrayRowColumn(result.dimension, subscript);
                const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(array.dimension[0], array.dimension[1], n);
                result.array[i][j] = array.array[p][q];
            }
        });
        MultiArray.setType(result);
        return result;
    }

    /**
     * Get selected items from MultiArray by linear indices or subscripts.
     * @param M Matrix.
     * @param id Identifier.
     * @param indexList
     * @returns MultiArray of selected items.
     */
    public static getElements(M: MultiArray, id: string, indexList: (ComplexDecimal | MultiArray)[]): MultiArray | ComplexDecimal {
        let result: MultiArray;
        if (indexList.length === 0) {
            return M;
        } else {
            const args = indexList.map((index) => MultiArray.linearize(index));
            const argsLength = args.map((arg) => arg.length);
            if (indexList.length === 1 && 'array' in indexList[0]) {
                result = new MultiArray(indexList[0].dimension);
            } else {
                result = new MultiArray(argsLength.length > 1 ? argsLength : [argsLength[0], 1]);
            }
            for (let n = 0; n < argsLength.reduce((p, c) => p * c, 1); n++) {
                const subscriptM = MultiArray.linearIndexToSubscript(argsLength, n).map((s, r) => args[r][s - 1]);
                const linearM = MultiArray.parseSubscript(M.dimension, subscriptM, id);
                const [i, j] = MultiArray.linearIndexToMultiArrayRowColumn(M.dimension[0], M.dimension[1], linearM);
                const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(result.dimension[0], result.dimension[1], n);
                result.array[p][q] = M.array[i][j];
            }
            MultiArray.setType(result);
            return MultiArray.MultiArrayToScalar(result);
        }
    }

    /**
     * Get selected items from MultiArray by logical indexing.
     * @param M Matrix.
     * @param id Identifier.
     * @param items Logical index.
     * @returns MultiArray of selected items.
     */
    public static getElementsLogical(M: MultiArray, id: string, items: MultiArray): MultiArray | ComplexDecimal {
        const result = new MultiArray();
        const linM = MultiArray.linearize(M);
        const test = MultiArray.linearize(items).map((value: ComplexDecimal) => value.re.toNumber());
        if (test.length > linM.length) {
            const dimM = M.dimension.slice();
            throw new EvalError(`${id}(${test.length}): out of bound ${linM.length} (dimensions are ${dimM.join('x')})`);
        }
        for (let n = 0; n < linM.length; n++) {
            if (test[n]) {
                result.array.push([linM[n]]);
            }
        }
        result.dimension = [result.array.length, 1];
        return MultiArray.MultiArrayToScalar(result);
    }

    /**
     * Set selected items from MultiArray by linear index or subscripts.
     * @param nameTable Name Table.
     * @param id Identifier.
     * @param args Linear indices or subscripts.
     * @param right Value to assign.
     */
    public static setElements(nameTable: TNameTable, id: string, indexList: (ComplexDecimal | MultiArray)[], right: MultiArray, input?: string, that?: Evaluator): void {
        if (indexList.length === 0) {
            throw new RangeError('invalid empty index list.');
        } else {
            const linright = MultiArray.linearize(right);
            const isLinearIndex = indexList.length === 1;
            const args = indexList.map((index) => MultiArray.linearize(index));
            const argsLength = args.map((arg) => arg.length);
            const argsParsed = args.map((arg) =>
                arg.map((i) => MultiArray.testIndex(i, `${input ? input : ''}${that ? '(' + args.map((arg) => arg.map((i) => that.Unparse(i))).join() + ')' : ''}`)),
            );
            const argsMax = argsParsed.map((arg) => Math.max(...arg));
            if (linright.length !== 1 && linright.length !== argsLength.reduce((p, c) => p * c, 1)) {
                throw new RangeError(`=: nonconformant arguments (op1 is ${argsLength.join('x')}, op2 is ${right.dimension.join('x')})`);
            }
            if (typeof nameTable[id] !== 'undefined' && 'array' in nameTable[id].expr) {
                if (isLinearIndex) {
                    if (argsMax[0] > MultiArray.linearLength(nameTable[id].expr)) {
                        throw new RangeError('Invalid resizing operation or ambiguous assignment to an out-of-bounds array element.');
                    }
                } else {
                    MultiArray.expand(nameTable[id].expr, argsMax);
                }
            } else {
                if (isLinearIndex) {
                    nameTable[id] = {
                        args: [],
                        expr: new MultiArray([1, argsMax[0]], ComplexDecimal.zero()),
                    };
                } else {
                    nameTable[id] = {
                        args: [],
                        expr: new MultiArray(argsMax, ComplexDecimal.zero()),
                    };
                }
            }
            const array: MultiArray = nameTable[id].expr;
            const dimension: number[] = nameTable[id].expr.dimension.slice();
            for (let n = 0; n < argsLength.reduce((p, c) => p * c, 1); n++) {
                const subscript = MultiArray.linearIndexToSubscript(argsLength, n);
                const subscriptArgs: number[] = subscript.map((s, r) =>
                    MultiArray.testIndex(args[r][s - 1], `${input ? input : ''}${that ? '(' + subscript.map((i) => that.Unparse(new ComplexDecimal(i))).join() + ')' : ''}`),
                );
                const indexLinear = MultiArray.subscriptToLinearIndex(dimension, subscriptArgs);
                const [p, q] = MultiArray.linearIndexToMultiArrayRowColumn(dimension[0], dimension[1], indexLinear);
                array.array[p][q] = linright.length === 1 ? linright[0] : linright[n];
            }
        }
    }

    /**
     * Set selected items from MultiArray by logical indexing.
     * @param nameTable Name Table.
     * @param id Identifier.
     * @param arg Logical index.
     * @param right Value to assign.
     */
    public static setElementsLogical(nameTable: TNameTable, id: string, arg: ComplexDecimal[], right: MultiArray): void {
        const linright = MultiArray.linearize(right);
        const test = arg.map((value: ComplexDecimal) => value.re.toNumber());
        const testCount = test.reduce((p, c) => p + c, 0);
        if (testCount !== linright.length) {
            throw new EvalError(`=: nonconformant arguments (op1 is ${testCount}x1, op2 is ${right.dimension[0]}x${right.dimension[1]})`);
        }
        const isDefinedId = typeof nameTable[id] !== 'undefined';
        const isNotFunction = isDefinedId && nameTable[id].args.length === 0;
        const isMultiArray = isNotFunction && 'array' in nameTable[id].expr;
        if (isMultiArray) {
            for (let j = 0, n = 0, r = 0; j < nameTable[id].expr.dimension[1]; j++) {
                for (let i = 0; i < nameTable[id].expr.dimension[0]; i++, r++) {
                    if (test[r]) {
                        nameTable[id].expr.array[i][j] = linright[n];
                        n++;
                    }
                }
            }
        } else {
            throw new EvalError(`${id}(_): invalid matrix indexing.`);
        }
    }
}
