import { ComplexDecimal } from './complex-decimal';

declare let EvaluatorPointer: any;

export type dimRange = {
    start: number;
    stride: number;
    stop: number;
};

export type ComplexDecimalRange = {
    start: ComplexDecimal;
    stride: ComplexDecimal;
    stop: ComplexDecimal;
};

export class MultiArray {
    dim: Array<number>;
    array: Array<Array<ComplexDecimal>>;

    static functions: { [name: string]: Function } = {
        size: MultiArray.size,
        sub2ind: MultiArray.sub2ind,
        ind2sub: MultiArray.ind2sub,
        zeros: MultiArray.zeros,
        ones: MultiArray.ones,
        eye: MultiArray.eye,
        inv: MultiArray.inv,
        det: MultiArray.det,
        trace: MultiArray.trace,
        rows: MultiArray.rows,
        cols: MultiArray.cols,
        minor: MultiArray.minor,
        cofactor: MultiArray.cofactor,
        adj: MultiArray.adj,
        pivot: MultiArray.pivot,
        lu: MultiArray.lu,
        min: MultiArray.min,
        max: MultiArray.max,
        mean: MultiArray.mean,
        horzcat: MultiArray.horzcat,
        vertcat: MultiArray.vertcat,
        gauss: MultiArray.gauss,
    };

    constructor(shape?: number[], fill?: ComplexDecimal) {
        if (!shape) {
            this.dim = [0, 0];
            this.array = [];
        } else if (!fill) {
            this.dim = shape.slice();
            this.array = new Array(this.dim[0]);
        } else {
            this.dim = shape.slice();
            this.array = new Array(this.dim[0]);
            for (let i = 0; i < this.dim[0]; i++) {
                this.array[i] = new Array(this.dim[1]).fill(fill);
                // for (let j = 0; j < this.dim[1]; j++) {
                // this.array[i][j] = new ComplexDecimal(fill.re, fill.im);
                // }
            }
        }
    }

    static firstRow(row: Array<any>): MultiArray {
        const result = new MultiArray([1, row.length]);
        result.array[0] = row;
        return result;
    }

    static appendRow(matrix: MultiArray, row: Array<any>): MultiArray {
        matrix.array.push(row);
        matrix.dim[0]++;
        return matrix;
    }

    static unparse(tree: MultiArray, that: any): string {
        let arraystr = '';
        for (let i = 0; i < tree.dim[0]; i++) {
            for (let j = 0; j < tree.dim[1]; j++) {
                arraystr += that.Unparse(tree.array[i][j]) + ',';
            }
            arraystr = arraystr.substring(0, arraystr.length - 1);
            arraystr += ';';
        }
        arraystr = arraystr.substring(0, arraystr.length - 1);
        return '[' + arraystr + ']';
    }

    static unparseML(tree: MultiArray, that: any): string {
        let temp = '';
        temp += '<mrow><mo>[</mo><mtable>';
        for (let i = 0; i < tree.dim[0]; i++) {
            temp += '<mtr>';
            for (let j = 0; j < tree.dim[1]; j++) {
                temp += '<mtd>' + that.unparserML(tree.array[i][j]) + '</mtd>';
            }
            temp += '</mtr>';
        }
        temp +=
            tree.array.length > 0
                ? '</mtable><mo>]</mo></mrow>'
                : "<mspace width='0.5em'/></mtable><mo>]</mo></mrow><mo>(</mo><mn>0</mn><mi>&times;</mi><mn>0</mn><mo>)</mo>";
        return temp;
    }

    static evaluate(tree: MultiArray, that: any, fname: string): MultiArray {
        const result = new MultiArray();
        for (let i = 0, k = 0; i < tree.array.length; i++, k++) {
            result.array.push([]);
            let h = 1;
            for (let j = 0; j < tree.array[i].length; j++) {
                const temp = that.Evaluator(tree.array[i][j], false, fname);
                if (MultiArray.isThis(temp)) {
                    if (j == 0) {
                        h = temp.array.length;
                        result.array.splice(k, 1, temp.array[0]);
                        for (let n = 1; n < h; n++) {
                            result.array.splice(k + n, 0, temp.array[n]);
                        }
                    } else {
                        for (let n = 0; n < temp.array.length; n++) {
                            for (let m = 0; m < temp.array[0].length; m++) {
                                result.array[k + n].push(temp.array[n][m]);
                            }
                        }
                    }
                } else {
                    result.array[k][j] = temp;
                }
            }
            k += h - 1;
            if (i != 0) {
                if (result.array[i].length != result.array[0].length)
                    throw new Error('vertical dimensions mismatch (' + k + 'x' + result.array[0].length + ' vs 1x' + result.array[i].length + ')');
            }
        }
        result.dim = [result.array.length, result.array.length ? result.array[0].length : 0];
        return result;
    }

    static isThis(obj: any): boolean {
        return 'array' in obj;
    }

    static isRange(obj: any): boolean {
        return 'start' in obj;
    }

    static mat_0x0(): MultiArray {
        return new MultiArray([0, 0]);
    }

    static arrayEqual(left: Array<number>, right: Array<number>): boolean {
        return !(left < right || left > right);
    }

    static isSameDim(left: MultiArray, right: MultiArray) {
        return !(left.dim < right.dim || left.dim > right.dim);
    }

    static number2matrix1x1(value: ComplexDecimal | MultiArray) {
        if ('array' in value) {
            return value;
        } else if ('re' in value) {
            const result = new MultiArray([1, 1]);
            result.array[0] = [value];
            return result;
            // return new MultiArray([1,1],value)
        }
    }

    static clone(M: MultiArray): MultiArray {
        const result = new MultiArray(M.dim);
        for (let i = 0; i < M.dim[0]; i++) {
            result.array[i] = new Array(M.dim[1]);
            for (let j = 0; j < M.dim[1]; j++) {
                result.array[i][j] = Object.assign({}, M.array[i][j]);
            }
        }
        return result;
    }

    static toLogical(M: MultiArray): ComplexDecimal {
        for (let i = 0; i < M.dim[0]; i++) {
            for (let j = 0; j < M.dim[1]; j++) {
                const value = ComplexDecimal.toMaxPrecision(M.array[i][j]);
                if (value.re.eq(0) && value.im.eq(0)) {
                    return ComplexDecimal.false();
                }
            }
        }
        return ComplexDecimal.true();
    }

    static map(M: MultiArray, f: Function): MultiArray {
        const result = new MultiArray(M.dim.slice());
        for (let i = 0; i < M.dim[0]; i++) {
            result.array[i] = M.array[i].map(f as any);
        }
        return result;
    }

    static expandRange(startNode: ComplexDecimal, stopNode: ComplexDecimal, strideNode?: ComplexDecimal | null): MultiArray {
        const temp = [];
        const s = strideNode ? strideNode.re.toNumber() : 1;
        for (let n = startNode.re.toNumber(), i = 0; n <= stopNode.re.toNumber(); n += s, i++) {
            temp[i] = new ComplexDecimal(n, 0);
        }
        const result = new MultiArray([1, temp.length]);
        result.array = [temp];
        return result;
    }

    static testIndex(k: ComplexDecimal, bound: number, matrix: MultiArray, input: string): number {
        if (!k.re.isInteger() || !k.re.gte(1)) throw new Error(input + ': subscripts must be either integers greater than or equal 1 or logicals');
        if (!k.im.eq(0)) throw new Error(input + ': subscripts must be real');
        const result = k.re.toNumber() - 1;
        if (result >= bound) {
            throw new Error(input + ': out of bound ' + bound + ' (dimensions are ' + matrix.dim[0] + 'x' + matrix.dim[1] + ')');
        }
        return result;
    }

    static oneRowToDim(M: Array<ComplexDecimal> | MultiArray): Array<number> {
        if (Array.isArray(M)) {
            const result = [];
            for (let i = 0; i < M.length; i++) {
                result[i] = M[i].re.toNumber();
            }
            return result;
        } else {
            const result = [];
            for (let i = 0; i < M.array[0].length; i++) {
                result[i] = M.array[0][i].re.toNumber();
            }
            return result;
        }
    }

    static subMatrix(temp: MultiArray, id: string, argumentsList: Array<any>): any {
        if (argumentsList.length == 1) {
            // single value indexing
            if ('array' in argumentsList[0]) {
                const result = new MultiArray(argumentsList[0].dim.slice(0, 2));
                for (let i = 0; i < argumentsList[0].dim[0]; i++) {
                    result.array[i] = new Array(argumentsList[0].dim[1]);
                    for (let j = 0; j < argumentsList[0].dim[1]; j++) {
                        const n = MultiArray.testIndex(
                            argumentsList[0].array[i][j],
                            temp.dim[0] * temp.dim[1],
                            temp,
                            id + '(' + argumentsList[0].array[i][j].re.toNumber() + ')',
                        );
                        result.array[i][j] = temp.array[n % temp.dim[1]][Math.floor(n / temp.dim[1])];
                    }
                }
                return result;
            } else {
                const n = MultiArray.testIndex(argumentsList[0], temp.dim[0] * temp.dim[1], temp, id + '(' + argumentsList[0].re.toNumber() + ')');
                return temp.array[n % temp.dim[1]][Math.floor(n / temp.dim[1])];
            }
        } else if (argumentsList.length == 2) {
            // double value indexing
            argumentsList[0] = MultiArray.number2matrix1x1(argumentsList[0]);
            argumentsList[1] = MultiArray.number2matrix1x1(argumentsList[1]);
            const rows = argumentsList[0].dim[0] * argumentsList[0].dim[1];
            const cols = argumentsList[1].dim[0] * argumentsList[1].dim[1];
            const result = new MultiArray([rows, cols]);
            let s = 0;
            for (let j = 0; j < argumentsList[0].dim[1]; j++) {
                for (let i = 0; i < argumentsList[0].dim[0]; i++) {
                    const p = MultiArray.testIndex(
                        argumentsList[0].array[i][j],
                        temp.dim[0],
                        temp,
                        id + '(' + argumentsList[0].array[i][j].re.toNumber() + ',_)',
                    );
                    for (let n = 0; n < argumentsList[1].dim[1]; n++) {
                        for (let m = 0; m < argumentsList[1].dim[0]; m++) {
                            const q = MultiArray.testIndex(
                                argumentsList[1].array[m][n],
                                temp.dim[1],
                                temp,
                                id + '(_,' + argumentsList[1].array[m][n].re.toNumber() + ')',
                            );
                            if (!(s % cols)) {
                                result.array[Math.floor(s / cols)] = new Array(cols);
                            }
                            result.array[Math.floor(s / cols)][s % cols] = temp.array[p][q];
                            s++;
                        }
                    }
                }
            }
            if (result.dim[0] == 1 && result.dim[1] == 1) {
                return result.array[0][0];
            } else {
                return result;
            }
        } else {
            throw new Error(id + '(_,_,...): out of bounds (dimensions are ' + temp.dim[0] + 'x' + temp.dim[1] + ')');
        }
    }

    static mul(left: MultiArray, right: MultiArray): MultiArray {
        if (left.dim[1] == right.dim[0]) {
            //matrix multiplication
            const result = new MultiArray([left.dim[0], right.dim[1]]);
            for (let i = 0; i < left.dim[0]; i++) {
                result.array[i] = new Array(right.dim[1]).fill(ComplexDecimal.zero());
                for (let j = 0; j < right.dim[1]; j++) {
                    for (let n = 0; n < left.dim[1]; n++) {
                        result.array[i][j] = ComplexDecimal.add(result.array[i][j], ComplexDecimal.mul(left.array[i][n], right.array[n][j]));
                    }
                }
            }
            return result;
        } else {
            throw new Error(
                'operator *: nonconformant arguments (op1 is ' +
                    left.dim[0] +
                    'x' +
                    left.dim[1] +
                    ', op2 is ' +
                    right.dim[0] +
                    'x' +
                    right.dim[1] +
                    ')',
            );
        }
    }

    static scalarOpMultiArray(
        op: 'add' | 'sub' | 'mul' | 'rdiv' | 'ldiv' | 'pow' | 'lt' | 'lte' | 'eq' | 'gte' | 'gt' | 'ne' | 'and' | 'or',
        left: ComplexDecimal,
        right: MultiArray,
    ): MultiArray {
        const result = new MultiArray(right.dim);
        for (let i = 0; i < result.dim[0]; i++) {
            result.array[i] = new Array(result.dim[1]);
            for (let j = 0; j < result.dim[1]; j++) {
                result.array[i][j] = ComplexDecimal[op](left, right.array[i][j]);
            }
        }
        return result;
    }

    static MultiArrayOpScalar(
        op: 'add' | 'sub' | 'mul' | 'rdiv' | 'ldiv' | 'pow' | 'lt' | 'lte' | 'eq' | 'gte' | 'gt' | 'ne' | 'and' | 'or',
        left: MultiArray,
        right: ComplexDecimal,
    ): MultiArray {
        const result = new MultiArray(left.dim);
        for (let i = 0; i < result.dim[0]; i++) {
            result.array[i] = new Array(result.dim[1]);
            for (let j = 0; j < result.dim[1]; j++) {
                result.array[i][j] = ComplexDecimal[op](left.array[i][j], right);
            }
        }
        return result;
    }

    static leftOp(op: 'clone' | 'neg' | 'not', right: MultiArray): MultiArray {
        const result = new MultiArray(right.dim);
        for (let i = 0; i < result.dim[0]; i++) {
            result.array[i] = new Array(result.dim[1]);
            for (let j = 0; j < result.dim[1]; j++) {
                result.array[i][j] = ComplexDecimal[op](right.array[i][j]);
            }
        }
        return result;
    }

    static ewiseOp(
        op: 'add' | 'sub' | 'mul' | 'rdiv' | 'ldiv' | 'pow' | 'lt' | 'lte' | 'eq' | 'gte' | 'gt' | 'ne' | 'and' | 'or',
        left: MultiArray,
        right: MultiArray,
    ): MultiArray {
        if (MultiArray.arrayEqual(left.dim.slice(0, 2), right.dim.slice(0, 2))) {
            // left and right has same number of rows and columns
            const result = new MultiArray(left.dim);
            for (let i = 0; i < result.dim[0]; i++) {
                result.array[i] = new Array(result.dim[1]);
                for (let j = 0; j < result.dim[1]; j++) {
                    result.array[i][j] = ComplexDecimal[op](left.array[i][j], right.array[i][j]);
                }
            }
            return result;
        } else if (left.dim[0] == right.dim[0]) {
            // left and right has same number of rows
            let col, matrix;
            if (left.dim[1] == 1) {
                // left has one column
                col = left;
                matrix = right;
            } else if (right.dim[1] == 1) {
                // right has one column
                col = right;
                matrix = left;
            } else {
                throw new Error(
                    'operator ' +
                        op +
                        ': nonconformant arguments (op1 is ' +
                        left.dim[0] +
                        'x' +
                        left.dim[1] +
                        ', op2 is ' +
                        right.dim[0] +
                        'x' +
                        right.dim[1] +
                        ')',
                );
            }
            const result = new MultiArray([col.dim[0], matrix.dim[1]]);
            for (let i = 0; i < col.dim[0]; i++) {
                result.array[i] = new Array(matrix.dim[1]);
                for (let j = 0; j < matrix.dim[1]; j++) {
                    result.array[i][j] = ComplexDecimal[op](col.array[i][0], matrix.array[i][j]);
                }
            }
            return result;
        } else if (left.dim[1] == right.dim[1]) {
            // left and right has same number of columns
            let row, matrix;
            if (left.dim[0] == 1) {
                // left has one row
                row = left;
                matrix = right;
            } else if (right.dim[0] == 1) {
                // right has one row
                row = right;
                matrix = left;
            } else {
                throw new Error(
                    'operator ' +
                        op +
                        ': nonconformant arguments (op1 is ' +
                        left.dim[0] +
                        'x' +
                        left.dim[1] +
                        ', op2 is ' +
                        right.dim[0] +
                        'x' +
                        right.dim[1] +
                        ')',
                );
            }
            const result = new MultiArray([matrix.dim[0], row.dim[1]]);
            for (let i = 0; i < matrix.dim[0]; i++) {
                result.array[i] = new Array(row.dim[1]);
                for (let j = 0; j < row.dim[1]; j++) {
                    result.array[i][j] = ComplexDecimal[op](row.array[0][j], matrix.array[i][j]);
                }
            }
            return result;
        } else if (left.dim[0] == 1 && right.dim[1] == 1) {
            // left has one row and right has one column
            const result = new MultiArray([right.dim[0], left.dim[1]]);
            for (let i = 0; i < right.dim[0]; i++) {
                result.array[i] = new Array(left.dim[1]);
                for (let j = 0; j < left.dim[1]; j++) {
                    result.array[i][j] = ComplexDecimal[op](left.array[0][j], right.array[i][0]);
                }
            }
            return result;
        } else if (left.dim[1] == 1 && right.dim[0] == 1) {
            // left has one column and right has one row
            const result = new MultiArray([left.dim[0], right.dim[1]]);
            for (let i = 0; i < left.dim[0]; i++) {
                result.array[i] = new Array(right.dim[1]);
                for (let j = 0; j < right.dim[1]; j++) {
                    result.array[i][j] = ComplexDecimal[op](left.array[i][0], right.array[0][j]);
                }
            }
            return result;
        } else {
            throw new Error(
                'operator ' +
                    op +
                    ': nonconformant arguments (op1 is ' +
                    left.dim[0] +
                    'x' +
                    left.dim[1] +
                    ', op2 is ' +
                    right.dim[0] +
                    'x' +
                    right.dim[1] +
                    ')',
            );
        }
    }

    static inv(M: MultiArray): MultiArray {
        // Returns the inverse of matrix `M`.
        // from http://blog.acipo.com/matrix-inversion-in-javascript/
        // I use Guassian Elimination to calculate the inverse:
        // (1) 'augment' the matrix (left) by the identity (on the right)
        // (2) Turn the matrix on the left into the identity by elemetry row ops
        // (3) The matrix on the right is the inverse (was the identity matrix)
        // There are 3 elemtary row ops: (I combine b and c in my code)
        // (a) Swap 2 rows
        // (b) Multiply a row by a scalar
        // (c) Add 2 rows

        //if the matrix isn't square: exit (error)
        if (M.dim[0] == M.dim[1]) {
            //create the identity matrix (I), and a clone (C) of the original
            let i = 0,
                ii = 0,
                j = 0,
                e = ComplexDecimal.zero();
            const dim = M.dim[0];
            const I: Array<Array<ComplexDecimal>> = [],
                C: Array<Array<any>> = [];
            for (i = 0; i < dim; i += 1) {
                // Create the row
                I[I.length] = [];
                C[C.length] = [];
                for (j = 0; j < dim; j += 1) {
                    //if we're on the diagonal, put a 1 (for identity)
                    if (i == j) {
                        I[i][j] = ComplexDecimal.one();
                    } else {
                        I[i][j] = ComplexDecimal.zero();
                    }
                    // Also, make the clone of the original
                    C[i][j] = M.array[i][j];
                }
            }

            // Perform elementary row operations
            for (i = 0; i < dim; i += 1) {
                // get the element e on the diagonal
                e = C[i][i];

                // if we have a 0 on the diagonal (we'll need to swap with a lower row)
                if (e.re.eq(0) && e.im.eq(0)) {
                    //look through every row below the i'th row
                    for (ii = i + 1; ii < dim; ii += 1) {
                        //if the ii'th row has a non-0 in the i'th col
                        if (!C[ii][i].re.eq(0) && !C[ii][i].im.eq(0)) {
                            //it would make the diagonal have a non-0 so swap it
                            for (j = 0; j < dim; j++) {
                                e = C[i][j]; //temp store i'th row
                                C[i][j] = C[ii][j]; //replace i'th row by ii'th
                                C[ii][j] = e; //repace ii'th by temp
                                e = I[i][j]; //temp store i'th row
                                I[i][j] = I[ii][j]; //replace i'th row by ii'th
                                I[ii][j] = e; //repace ii'th by temp
                            }
                            //don't bother checking other rows since we've swapped
                            break;
                        }
                    }
                    //get the new diagonal
                    e = C[i][i];
                    //if it's still 0, not invertable (error)
                    if (e.re.eq(0) && e.im.eq(0)) {
                        return new MultiArray([M.dim[0], M.dim[1]], ComplexDecimal.inf_0());
                    }
                }

                // Scale this row down by e (so we have a 1 on the diagonal)
                for (j = 0; j < dim; j++) {
                    C[i][j] = ComplexDecimal.rdiv(C[i][j], e); //apply to original matrix
                    I[i][j] = ComplexDecimal.rdiv(I[i][j], e); //apply to identity
                }

                // Subtract this row (scaled appropriately for each row) from ALL of
                // the other rows so that there will be 0's in this column in the
                // rows above and below this one
                for (ii = 0; ii < dim; ii++) {
                    // Only apply to other rows (we want a 1 on the diagonal)
                    if (ii == i) {
                        continue;
                    }

                    // We want to change this element to 0
                    e = C[ii][i];

                    // Subtract (the row above(or below) scaled by e) from (the
                    // current row) but start at the i'th column and assume all the
                    // stuff left of diagonal is 0 (which it should be if we made this
                    // algorithm correctly)
                    for (j = 0; j < dim; j++) {
                        C[ii][j] = ComplexDecimal.sub(C[ii][j], ComplexDecimal.mul(e, C[i][j])); //apply to original matrix
                        I[ii][j] = ComplexDecimal.sub(I[ii][j], ComplexDecimal.mul(e, I[i][j])); //apply to identity
                    }
                }
            }

            //we've done all operations, C should be the identity
            //matrix I should be the inverse:
            const result = new MultiArray(M.dim);
            result.array = I;
            return result;
        } else {
            throw new Error('inverse: A must be a square matrix');
        }
    }

    static zeros(...args: any): MultiArray | ComplexDecimal {
        if (!args.length) {
            return ComplexDecimal.zero();
        } else if (args.length == 1) {
            if ('re' in args[0]) {
                return new MultiArray([args[0].re.toNumber(), args[0].re.toNumber()], ComplexDecimal.zero());
            } else {
                return new MultiArray(MultiArray.oneRowToDim(args[0]), ComplexDecimal.zero());
            }
        } else {
            return new MultiArray(MultiArray.oneRowToDim(args), ComplexDecimal.zero());
        }
    }

    static ones(...args: any): MultiArray | ComplexDecimal {
        if (!args.length) {
            return ComplexDecimal.one();
        } else if (args.length == 1) {
            if ('re' in args[0]) {
                return new MultiArray([args[0].re.toNumber(), args[0].re.toNumber()], ComplexDecimal.one());
            } else {
                return new MultiArray(MultiArray.oneRowToDim(args[0]), ComplexDecimal.one());
            }
        } else {
            return new MultiArray(MultiArray.oneRowToDim(args), ComplexDecimal.one());
        }
    }

    static eye(i: ComplexDecimal, j?: ComplexDecimal): MultiArray {
        if (!i.re.isInteger()) throw new Error('Invalid call to eye. Non integer number argument.');
        const temp = new MultiArray([i.re.toNumber(), j ? j.re.toNumber() : i.re.toNumber()], ComplexDecimal.zero());
        for (let n = 0; n < Math.min(i.re.toNumber(), j ? j.re.toNumber() : i.re.toNumber()); n++) {
            temp.array[n][n] = ComplexDecimal.one();
        }
        return temp;
    }

    static pow(left: MultiArray, right: ComplexDecimal): MultiArray {
        let temp1; // matrix power (multiple multiplication)
        if (right.re.isInteger() && right.im.eq(0)) {
            if (right.re.eq(0)) {
                temp1 = MultiArray.eye(new ComplexDecimal(left.dim[0], 0));
            } else if (right.re.gt(0)) {
                temp1 = MultiArray.clone(left);
            } else {
                temp1 = MultiArray.inv(left);
            }
            if (Math.abs(right.re.toNumber()) != 1) {
                let temp2 = MultiArray.clone(temp1);
                for (let i = 1; i < Math.abs(right.re.toNumber()); i++) {
                    temp2 = MultiArray.mul(temp2, temp1);
                }
                temp1 = temp2;
            }
            return temp1;
        } else {
            throw new Error("exponent must be integer real in matrix '^'");
        }
    }

    static transpose(left: MultiArray): MultiArray {
        const result = new MultiArray(left.dim.slice(0, 2).reverse());
        for (let i = 0; i < left.dim[1]; i++) {
            result.array[i] = new Array(left.dim[0]);
            for (let j = 0; j < left.dim[0]; j++) {
                result.array[i][j] = Object.assign({}, left.array[j][i]);
            }
        }
        return result;
    }

    static ctranspose(left: MultiArray): MultiArray {
        const result = new MultiArray(left.dim.slice(0, 2).reverse());
        for (let i = 0; i < left.dim[1]; i++) {
            result.array[i] = new Array(left.dim[0]);
            for (let j = 0; j < left.dim[0]; j++) {
                result.array[i][j] = ComplexDecimal.conj(left.array[j][i]);
            }
        }
        return result;
    }

    static horzcat(L: MultiArray, R: MultiArray): MultiArray {
        if (L.dim[0] == R.dim[0]) {
            const temp = new MultiArray([L.dim[0], L.dim[1] + R.dim[1]]);
            for (let i = 0; i < L.dim[0]; i++) {
                temp.array[i] = [];
                for (let j = 0; j < L.dim[1]; j++) {
                    temp.array[i][j] = Object.assign({}, L.array[i][j]);
                }
                for (let j = 0; j < R.dim[1]; j++) {
                    temp.array[i][j + L.dim[1]] = Object.assign({}, R.array[i][j]);
                }
            }
            return temp;
        } else {
            throw new Error('invalid dimensions in horzcat function');
        }
    }

    static vertcat(U: MultiArray, D: MultiArray): MultiArray {
        if (U.dim[1] == D.dim[1]) {
            const temp = new MultiArray([U.dim[0] + D.dim[0], U.dim[1]]);
            for (let i = 0; i < U.dim[0]; i++) {
                temp.array[i] = [];
                for (let j = 0; j < U.dim[1]; j++) {
                    temp.array[i][j] = Object.assign({}, U.array[i][j]);
                }
            }
            for (let i = 0; i < D.dim[0]; i++) {
                temp.array[i + U.dim[0]] = [];
                for (let j = 0; j < D.dim[1]; j++) {
                    temp.array[i + U.dim[0]][j] = Object.assign({}, D.array[i][j]);
                }
            }
            return temp;
        } else {
            throw new Error('invalid dimensions in vertcat function');
        }
    }

    static det(M: MultiArray): ComplexDecimal {
        if (M.dim[0] == M.dim[1]) {
            let det = ComplexDecimal.zero();
            if (M.dim[0] == 1) det = M.array[0][0];
            else if (M.dim[0] == 2)
                det = ComplexDecimal.sub(ComplexDecimal.mul(M.array[0][0], M.array[1][1]), ComplexDecimal.mul(M.array[0][1], M.array[1][0]));
            else {
                det = ComplexDecimal.zero();
                for (let j1 = 0; j1 < M.dim[0]; j1++) {
                    const m = new MultiArray([M.dim[0] - 1, M.dim[0] - 1], ComplexDecimal.zero());
                    for (let i = 1; i < M.dim[0]; i++) {
                        let j2 = 0;
                        for (let j = 0; j < M.dim[0]; j++) {
                            if (j == j1) continue;
                            m.array[i - 1][j2] = M.array[i][j];
                            j2++;
                        }
                    }
                    det = ComplexDecimal.add(
                        det,
                        ComplexDecimal.mul(new ComplexDecimal(Math.pow(-1, 2.0 + j1), 0), ComplexDecimal.mul(M.array[0][j1], MultiArray.det(m))),
                    );
                }
            }
            return det;
        } else {
            throw new Error('det: A must be a square matrix');
        }
    }

    static trace(M: MultiArray): ComplexDecimal {
        if (M.dim[0] == M.dim[1]) {
            let temp: ComplexDecimal = ComplexDecimal.zero();
            for (let i = 0; i < M.dim[0]; i++) {
                temp = ComplexDecimal.add(temp, M.array[i][i]);
            }
            return temp;
        } else {
            throw new Error('trace: invalid dimensions');
        }
    }

    static rows(M: MultiArray): ComplexDecimal {
        return new ComplexDecimal(M.dim[0], 0);
    }

    static cols(M: MultiArray): ComplexDecimal {
        return new ComplexDecimal(M.dim[1], 0);
    }

    static minor(M: MultiArray, p: ComplexDecimal, q: ComplexDecimal): MultiArray {
        // minor of matrix (remove line and column)
        const temp = MultiArray.clone(M);
        temp.array.splice(p.re.toNumber() - 1, 1);
        for (let i = 0; i < M.dim[0] - 1; i++) {
            temp.array[i].splice(q.re.toNumber() - 1, 1);
        }
        temp.dim[0]--;
        temp.dim[1]--;
        return temp;
    }

    static cofactor(M: MultiArray): MultiArray {
        const temp = new MultiArray([M.dim[0], M.dim[1]], ComplexDecimal.zero());
        for (let i = 0; i < M.dim[0]; i++) {
            for (let j = 0; j < M.dim[1]; j++) {
                const minor = MultiArray.minor(M, new ComplexDecimal(i + 1, 0), new ComplexDecimal(j + 1, 0));
                let sign: ComplexDecimal;
                if ((i + j) % 2 == 0) {
                    sign = ComplexDecimal.one();
                } else {
                    sign = ComplexDecimal.minusone();
                }
                temp.array[i][j] = ComplexDecimal.mul(sign, MultiArray.det(minor));
            }
        }
        return temp;
    }

    static min(M: MultiArray): MultiArray | ComplexDecimal {
        let temp: MultiArray;
        if (M.dim[0] === 1) {
            temp = MultiArray.transpose(M);
        } else {
            temp = M;
        }
        const result = new MultiArray([1, temp.dim[1]]);
        // result.array = new Array(temp.dim[0]);
        // result.array[0] = new Array();
        result.array = [new Array(temp.dim[1])];
        for (let j = 0; j < temp.dim[1]; j++) {
            result.array[0][j] = temp.array[0][j];
            for (let i = 1; i < temp.dim[0]; i++) {
                result.array[0][j] = ComplexDecimal.min(result.array[0][j], temp.array[i][j]);
            }
        }
        if (temp.dim[1] === 1) {
            return result.array[0][0];
        } else {
            return result;
        }
    }

    static max(M: MultiArray): MultiArray | ComplexDecimal {
        let temp: MultiArray;
        if (M.dim[0] === 1) {
            temp = MultiArray.transpose(M);
        } else {
            temp = M;
        }
        const result = new MultiArray([1, temp.dim[1]]);
        // result.array = new Array(temp.dim[0]);
        result.array = [new Array(temp.dim[1])];
        for (let j = 0; j < temp.dim[1]; j++) {
            result.array[0][j] = temp.array[0][j];
            for (let i = 1; i < temp.dim[0]; i++) {
                result.array[0][j] = ComplexDecimal.max(result.array[0][j], temp.array[i][j]);
            }
        }
        if (temp.dim[1] === 1) {
            return result.array[0][0];
        } else {
            return result;
        }
    }

    static mean(M: MultiArray): MultiArray | ComplexDecimal {
        let temp: MultiArray;
        if (M.dim[0] === 1) {
            temp = MultiArray.transpose(M);
        } else {
            temp = M;
        }
        const result = new MultiArray([1, temp.dim[1]]);
        // result.array = new Array(temp.dim[0]);
        result.array = [new Array(temp.dim[1])];
        for (let j = 0; j < temp.dim[1]; j++) {
            result.array[0][j] = temp.array[0][j];
            for (let i = 1; i < temp.dim[0]; i++) {
                result.array[0][j] = ComplexDecimal.add(result.array[0][j], temp.array[i][j]);
            }
            result.array[0][j] = ComplexDecimal.rdiv(result.array[0][j], new ComplexDecimal(temp.dim[0], 0));
        }
        if (temp.dim[1] === 1) {
            return result.array[0][0];
        } else {
            return result;
        }
    }

    static gauss(M: MultiArray, x: MultiArray): MultiArray | undefined {
        // Gaussian elimination algorithm for solving systems of linear equations.
        // Adapted from: https://github.com/itsravenous/gaussian-elimination
        if (M.dim[0] != M.dim[1]) throw new Error('invalid dimensions in function gauss');
        const A: MultiArray = MultiArray.clone(M);
        let i: number, k: number, j: number;
        const DMin = Math.min(x.dim[0], x.dim[1]);
        if (DMin == x.dim[1]) {
            x = MultiArray.transpose(x);
        }

        // Just make a single matrix
        for (i = 0; i < A.dim[0]; i++) {
            A.array[i].push(x.array[0][i]);
        }
        const n = A.dim[0];

        for (i = 0; i < n; i++) {
            // Search for maximum in this column
            let maxEl = ComplexDecimal.abs(A.array[i][i]),
                maxRow = i;
            for (k = i + 1; k < n; k++) {
                if (ComplexDecimal.abs(A.array[k][i]).re.gt(maxEl.re)) {
                    maxEl = ComplexDecimal.abs(A.array[k][i]);
                    maxRow = k;
                }
            }

            // Swap maximum row with current row (column by column)
            for (k = i; k < n + 1; k++) {
                const tmp = A.array[maxRow][k];
                A.array[maxRow][k] = A.array[i][k];
                A.array[i][k] = tmp;
            }

            // Make all rows below this one 0 in current column
            for (k = i + 1; k < n; k++) {
                const c = ComplexDecimal.rdiv(ComplexDecimal.neg(A.array[k][i]), A.array[i][i]);
                for (j = i; j < n + 1; j++) {
                    if (i === j) {
                        A.array[k][j] = ComplexDecimal.zero();
                    } else {
                        A.array[k][j] = ComplexDecimal.add(A.array[k][j], ComplexDecimal.mul(c, A.array[i][j]));
                    }
                }
            }
        }

        // Solve equation Ax=b for an upper triangular matrix A
        const X = new MultiArray([1, n], ComplexDecimal.zero());
        for (i = n - 1; i > -1; i--) {
            X.array[0][i] = ComplexDecimal.rdiv(A.array[i][n], A.array[i][i]);
            for (k = i - 1; k > -1; k--) {
                A.array[k][n] = ComplexDecimal.sub(A.array[k][n], ComplexDecimal.mul(A.array[k][i], X.array[0][i]));
            }
        }

        return X;
    }

    static lu(M: MultiArray): any {
        const n = M.dim[0];
        const lower = new MultiArray([M.dim[0], M.dim[1]], ComplexDecimal.zero());
        const upper = new MultiArray([M.dim[0], M.dim[1]], ComplexDecimal.zero());
        // Decomposing matrix into Upper and
        // Lower triangular matrix
        for (let i = 0; i < n; i++) {
            // Upper Triangular
            for (let k = i; k < n; k++) {
                // Summation of L(i, j) * U(j, k)
                let sum: ComplexDecimal = ComplexDecimal.zero();
                for (let j = 0; j < i; j++) {
                    sum = ComplexDecimal.add(sum, ComplexDecimal.mul(lower.array[i][j], upper.array[j][k]));
                }
                // Evaluating U(i, k)
                upper.array[i][k] = ComplexDecimal.sub(M.array[i][k], sum);
            }
            // Lower Triangular
            for (let k = i; k < n; k++) {
                if (i == k) {
                    // Diagonal as 1
                    lower.array[i][i] = ComplexDecimal.one();
                } else {
                    // Summation of L(k, j) * U(j, i)
                    let sum: ComplexDecimal = ComplexDecimal.zero();
                    for (let j = 0; j < i; j++) {
                        sum = ComplexDecimal.add(sum, ComplexDecimal.mul(lower.array[k][j], upper.array[j][i]));
                    }
                    // Evaluating L(k, i)
                    lower.array[k][i] = ComplexDecimal.rdiv(ComplexDecimal.sub(M.array[k][i], sum), upper.array[i][i]);
                }
            }
        }
        return EvaluatorPointer.nodeOp('*', lower, upper);
    }

    static pivot(M: MultiArray): MultiArray {
        const n = M.dim[0];
        const id = MultiArray.eye(new ComplexDecimal(n, 0));
        for (let i = 0; i < n; i++) {
            let maxm = ComplexDecimal.abs(M.array[i][i]);
            let row = i;
            for (let j = i; j < n; j++)
                if (ComplexDecimal.abs(M.array[j][i]).re.gt(maxm.re)) {
                    maxm = ComplexDecimal.abs(M.array[j][i]);
                    row = j;
                }
            if (i != row) {
                const tmp = id.array[i];
                id.array[i] = id.array[row];
                id.array[row] = tmp;
            }
        }
        return id;
    }

    static adj(M: MultiArray): MultiArray {
        return MultiArray.ctranspose(MultiArray.cofactor(M));
    }

    static size(X: any, DIM: any) {
        if (arguments.length == 1) {
            if ('re' in X) {
                return { array: [[ComplexDecimal.one(), ComplexDecimal.one()]] };
            } else if ('array' in X) {
                return { array: [[new ComplexDecimal(X.dim[0], 0), new ComplexDecimal(X.dim[1], 0)]] };
            }
        } else if (arguments.length == 2) {
            if ('re' in DIM && DIM.im.eq(0)) {
                if ('re' in X) {
                    return ComplexDecimal.one();
                } else if ('array' in X) {
                    if (Math.trunc(DIM.re.toNumber()) == 1) {
                        return new ComplexDecimal(X.dim[0], 0);
                    } else if (Math.trunc(DIM.re.toNumber()) == 2) {
                        return new ComplexDecimal(X.dim[1], 0);
                    } else if (Math.trunc(DIM.re.toNumber()) > 2) {
                        return new ComplexDecimal(1, 0);
                    } else {
                        throw new Error('size: requested dimension DIM (= ' + Math.trunc(DIM.re.toNumber()) + ') out of range');
                    }
                }
            } else {
                throw new Error('size: DIM must be a positive integer');
            }
        } else {
            throw new Error('Invalid call to size.');
        }
    }

    static sub2ind(DIMS: any, ...S: any) {
        if (arguments.length > 1) {
            const n = DIMS;
            return new ComplexDecimal(1, 0);
        } else {
            throw new Error('Invalid call to sub2ind.');
        }
    }

    static ind2sub(DIMS: any, IND: any) {
        if (arguments.length == 2) {
            return new ComplexDecimal(1, 0);
        } else {
            throw new Error('Invalid call to ind2sub.');
        }
    }
}
