/**
 * MATLAB®/Octave like syntax parser/interpreter/compiler.
 */

import Parser from './parser.js';
import { constantsTable } from './constants-table';
import { substGreek } from './subst-greek';
import { CharString } from './char-string';
import { ComplexDecimal } from './complex-decimal';
import { MultiArray } from './multi-array';
import { Tensor } from './tensor';

/**
 * baseFunctionTable type
 */
export type TBaseFunctionTableEntry = {
    mapper?: boolean;
    ev?: Array<boolean>;
    func: Function;
};
export type TBaseFunctionTable = { [k: string]: TBaseFunctionTableEntry };

/**
 * TEvaluatorConfig type
 */
type TAliasNameTable = Record<string, RegExp>;
export type TEvaluatorConfig = {
    aliasTable?: TAliasNameTable;
    externalFuctionTable?: TBaseFunctionTable;
};

/**
 * Abstract Syntax Tree nodes
 */
export type NodeExpr = NodeName | NodeArgExpr | NodeOperation | NodeList;

interface NodeRange {
    start: NodeExpr | null;
    stop: NodeExpr | null;
    stride: NodeExpr | null;
}

interface PrimaryNode {
    type: string | number;
}

interface NodeReserved extends PrimaryNode {}

export interface NodeName extends PrimaryNode {
    type: 'NAME';
    id: string;
}

interface NodeCmdWList extends PrimaryNode {
    type: 'CmdWList';
    id: string;
    args: Array<CharString>;
}

export interface NodeArgExpr extends PrimaryNode {
    type: 'ARG';
    expr: NodeExpr;
    args: Array<NodeExpr>;
}

export type NodeOperation = UnaryOperation | BinaryOperation;

type UnaryOperation = UnaryOperationL | UnaryOperationR;

interface UnaryOperationR extends PrimaryNode {
    right: NodeExpr;
}

interface UnaryOperationL extends PrimaryNode {
    left: NodeExpr;
}

interface BinaryOperation extends PrimaryNode {
    left: NodeExpr;
    right: NodeExpr;
}

export interface NodeList {
    list: Array<NodeExpr>;
}

/**
 * External parser declarations (defined in parser body)
 */
declare global {
    /* eslint-disable-next-line  no-var */
    var EvaluatorPointer: any;
    const commandsTable: string[];
}

/**
 * Evaluator object
 * It is implemented as a class but cannot be instantiated more than one time
 * simultaneously.
 */
export class Evaluator {
    /**
     * After run Parser or Evaluate method, the exitStatus property will contains
     * exit state of method.
     */
    public static response = {
        INFO: -2,
        WARNING: -1,
        OK: 0,
        LEX_ERROR: 1,
        PARSER_ERROR: 2,
        EVAL_ERROR: 3,
    };

    public debug: boolean = false;
    private readonly nativeNameTable: { [k: string]: any } = {
        false: ComplexDecimal.false(),
        true: ComplexDecimal.true(),
        i: ComplexDecimal.onei(),
        I: ComplexDecimal.onei(),
        j: ComplexDecimal.onei(),
        J: ComplexDecimal.onei(),
        e: ComplexDecimal.e(),
        pi: ComplexDecimal.pi(),
        inf: ComplexDecimal.inf_0(),
        Inf: ComplexDecimal.inf_0(),
        NaN: ComplexDecimal.NaN_0(),
    };
    private nameTable: { [k: string]: any } = {};
    private aliasTable: TAliasNameTable;
    public baseFunctionTable: TBaseFunctionTable = {};
    public localTable: { [k: string]: any } = {};
    private readonly parser: { parse: (input: string) => any } = Parser;
    exitStatus: number = Evaluator.response.OK;
    exitMessage: string = '';
    private readonly opTable: { [k: string]: Function } = {
        '+': Tensor.plus,
        '-': Tensor.minus,
        '.*': Tensor.times,
        '*': Tensor.mtimes,
        './': Tensor.rdivide,
        '/': Tensor.mrdivide,
        '.\\': Tensor.ldivide,
        '\\': Tensor.mldivide,
        '.^': Tensor.power,
        '^': Tensor.mpower,
        '+_': Tensor.uplus,
        '-_': Tensor.uminus,
        ".'": Tensor.transpose,
        "'": Tensor.ctranspose,
        '<': Tensor.lt,
        '<=': Tensor.lte,
        '==': Tensor.eq,
        '>=': Tensor.gte,
        '>': Tensor.gt,
        '!=': Tensor.ne,
        '&': Tensor.and,
        '|': Tensor.or,
        '!': Tensor.not,
        '&&': Tensor.mand,
        '||': Tensor.mor,
    };

    readonly nodeString = CharString.parse;
    readonly isString = CharString.isThis;
    readonly unparseString = CharString.unparse;
    readonly unparseStringML = CharString.unparseML;
    readonly removeQuotes = CharString.removeQuotes;
    readonly nodeNumber = ComplexDecimal.parse;
    readonly newNumber = ComplexDecimal.newThis;
    readonly isNumber = ComplexDecimal.isThis;
    readonly unparseNumber = ComplexDecimal.unparse;
    readonly unparseNumberML = ComplexDecimal.unparseML;
    readonly isTensor = MultiArray.isThis;
    readonly isRange = MultiArray.isRange;
    readonly unparseTensor = MultiArray.unparse;
    readonly unparseTensorML = MultiArray.unparseML;
    readonly evaluateTensor = MultiArray.evaluate;
    readonly mapTensor = MultiArray.map;
    readonly subTensor = MultiArray.subMatrix;
    readonly expandRange = MultiArray.expandRange;
    readonly firstRow = MultiArray.firstRow;
    readonly appendRow = MultiArray.appendRow;
    readonly tensor0x0 = MultiArray.mat_0x0;

    /**
     * Evaluator object constructor
     * @param config Evaluator configuration
     */
    constructor(config?: TEvaluatorConfig) {
        global.EvaluatorPointer = this;
        /* Set opTable aliases */
        this.opTable['.+'] = this.opTable['+'];
        this.opTable['.-'] = this.opTable['-'];
        this.opTable['.÷'] = this.opTable['./'];
        this.opTable['÷'] = this.opTable['/'];
        this.opTable['**'] = this.opTable['^'];
        this.opTable['.**'] = this.opTable['.^'];
        this.opTable['~='] = this.opTable['!='];
        this.opTable['~'] = this.opTable['!'];
        /* Load nativeNameTable and constantsTable in nameTable */
        this.ReloadNativeTable();
        /* Define function operators */
        for (const func in Tensor.twoMoreOpFunction) {
            this.DefBinMoreOpFunction(func, Tensor.twoMoreOpFunction[func]);
        }
        for (const func in Tensor.binaryOpFunction) {
            this.DefBinOpFunction(func, Tensor.binaryOpFunction[func]);
        }
        for (const func in Tensor.unaryOpFunction) {
            this.DefUnOpFunction(func, Tensor.unaryOpFunction[func]);
        }
        /* Define function mappers */
        for (const func in ComplexDecimal.mapFunction) {
            this.defFunction(func, ComplexDecimal.mapFunction[func], true);
        }
        /* Define other functions */
        for (const func in ComplexDecimal.twoArgFunction) {
            this.defFunction(func, ComplexDecimal.twoArgFunction[func]);
        }
        for (const func in MultiArray.functions) {
            this.defFunction(func, MultiArray.functions[func]);
        }
        this.set(config);
    }

    set(config?: TEvaluatorConfig): void {
        if (config) {
            if (config.aliasTable) {
                this.aliasTable = config.aliasTable;
                this.aliasName = (name: string): string => {
                    let result = false;
                    let aliasname = '';
                    for (const i in this.aliasTable) {
                        if (this.aliasTable[i].test(name)) {
                            result = true;
                            aliasname = i;
                            break;
                        }
                    }
                    if (result) {
                        return aliasname;
                    } else {
                        return name;
                    }
                };
            } else {
                this.aliasName = (name: string): string => name;
            }
            if (config.externalFuctionTable) {
                Object.assign(this.baseFunctionTable, config.externalFuctionTable);
            }
        } else {
            this.aliasName = (name: string): string => name;
        }
    }

    aliasName: (name: string) => string = (name: string): string => name;

    ReloadNativeTable(): void {
        // Insert nativeNameTable in nameTable
        for (const name in this.nativeNameTable) {
            this.nameTable[name] = { args: [], expr: this.nativeNameTable[name] };
        }
        // Insert constantsTable in nameTable
        for (const name in constantsTable) {
            this.nameTable[constantsTable[name][0]] = { args: [], expr: this.newNumber(constantsTable[name][1], 0) };
        }
    }

    Restart(): void {
        this.nameTable = {};
        this.localTable = {};
        this.ReloadNativeTable();
    }

    Parse(input: string): any {
        return this.parser.parse(input);
    }

    nodeReserved(nodeid: string): NodeReserved {
        return { type: nodeid };
    }

    nodeName(nodeid: string): NodeName {
        return { type: 'NAME', id: nodeid.replace(/(\r\n|[\n\r])|[\ ]/gm, '') };
    }

    nodeCmdWList(nodename: NodeName, nodelist: NodeList): NodeCmdWList {
        return { type: 'CmdWList', id: nodename.id, args: nodelist ? (nodelist.list as any) : [] };
    }

    nodeArgExpr(nodeexpr: any, nodelist?: any): NodeArgExpr {
        return { type: 'ARG', expr: nodeexpr, args: nodelist ? nodelist.list : [] };
    }
    //https://www.mathworks.com/help/matlab/ref/end.html
    nodeRange(left: any, ...right: any): NodeRange {
        if (right.length == 1) {
            return { start: left, stop: right[0], stride: null };
        } else if (right.length == 2) {
            return { start: left, stop: right[1], stride: right[0] };
        } else {
            throw new Error('invalid range');
        }
    }

    nodeOp(op: string, data1: any, data2: any): NodeOperation {
        switch (op) {
            case '.+':
            case '+':
            case '.-':
            case '-':
            case '.*':
            case '*':
            case './':
            case '/':
            case '.÷':
            case '÷':
            case '.\\':
            case '\\':
            case '.^':
            case '^':
            case '.**':
            case '**':
            case '<':
            case '<=':
            case '==':
            case '>=':
            case '>':
            case '!=':
            case '~=':
            case '&':
            case '|':
            case '&&':
            case '||':
            case '=':
            case '+=':
            case '-=':
            case '*=':
            case '/=':
            case '\\=':
            case '^=':
            case '**=':
            case '.*=':
            case './=':
            case '.\\=':
            case '.^=':
            case '.**=':
            case '&=':
            case '|=':
                return { type: op, left: data1, right: data2 };
            case '()':
            case '!':
            case '~':
            case '+_':
            case '-_':
            case '++_':
            case '--_':
                return { type: op, right: data1 };
            case ".'":
            case "'":
            case '_++':
            case '_--':
                return { type: op, left: data1 };
            default:
                return { type: 'INVALID' } as NodeOperation;
        }
    }

    nodeListFirst(node?: any): NodeList {
        if (node) {
            return { list: [node] };
        } else {
            return { list: [] };
        }
    }

    nodeList(lnode: any, node: any): NodeList {
        lnode.list.push(node);
        return lnode;
    }

    nodeFirstRow(row: any): MultiArray {
        if (row) {
            return this.firstRow(row.list);
        } else {
            return this.tensor0x0();
        }
    }

    nodeAppendRow(matrix: any, row: any): MultiArray {
        if (row) {
            return this.appendRow(matrix, row.list);
        } else {
            return matrix;
        }
    }

    validateAssignment(tree: any): any {
        if (tree.type == 'NAME' || (tree.type == 'ARG' && tree.expr.type == 'NAME')) {
            return tree;
        } else {
            throw new Error('parse error: invalid left hand side of assignment.');
        }
    }

    DefUnOpFunction(name: string, func: Function) {
        this.baseFunctionTable[name] = {
            mapper: false,
            func: (...operand: any) => {
                if (operand.length == 1) {
                    return func(operand[0]);
                } else {
                    throw new Error(`Invalid call to ${name}.`);
                }
            },
        };
    }

    DefBinOpFunction(name: string, func: Function) {
        this.baseFunctionTable[name] = {
            mapper: false,
            func: (left: any, ...right: any) => {
                if (right.length == 1) {
                    return func(left, right[0]);
                } else {
                    throw new Error(`Invalid call to ${name}.`);
                }
            },
        };
    }

    DefBinMoreOpFunction(name: string, func: Function) {
        this.baseFunctionTable[name] = {
            mapper: false,
            func: (left: any, ...right: any) => {
                if (right.length == 1) {
                    return func(left, right[0]);
                } else if (right.length > 1) {
                    let result = func(left, right[0]);
                    for (let i = 1; i < right.length; i++) {
                        result = func(result, right[i]);
                    }
                    return result;
                } else {
                    throw new Error('Invalid call to "+name+".');
                }
            },
        };
    }

    defFunction(name: string, func: Function, map?: boolean) {
        this.baseFunctionTable[name] = {
            mapper: map ? true : false,
            func,
        };
    }

    Unparse(tree: any): string {
        try {
            if (tree === undefined) {
                return '';
            }
            if ('list' in tree) {
                let list = '';
                for (let i = 0; i < tree.list.length; i++) {
                    list += this.Unparse(tree.list[i]) + '\n';
                }
                return list;
            } else if (this.isNumber(tree)) {
                // NUMBER
                return this.unparseNumber(tree);
            } else if (this.isString(tree)) {
                // STRING
                return this.unparseString(tree);
            } else if (this.isTensor(tree)) {
                // MATRIX
                return this.unparseTensor(tree, this);
            } else if (this.isRange(tree)) {
                // RANGE
                if (tree.start && tree.stop) {
                    if (tree.stride) {
                        return this.Unparse(tree.start) + ':' + this.Unparse(tree.stride) + ':' + this.Unparse(tree.stop);
                    } else {
                        return this.Unparse(tree.start) + ':' + this.Unparse(tree.stop);
                    }
                } else {
                    return ':';
                }
            } else {
                let arglist: string;
                switch (tree.type) {
                    case ':':
                    case '!':
                    case '~':
                        return tree.type;
                    case '.+':
                    case '+':
                    case '.-':
                    case '-':
                    case '.*':
                    case '*':
                    case './':
                    case '/':
                    case '.÷':
                    case '÷':
                    case '.\\':
                    case '\\':
                    case '.^':
                    case '^':
                    case '.**':
                    case '**':
                    case '<':
                    case '<=':
                    case '==':
                    case '>=':
                    case '>':
                    case '!=':
                    case '~=':
                    case '&':
                    case '|':
                    case '&&':
                    case '||':
                    case '=':
                    case '+=':
                    case '-=':
                    case '*=':
                    case '/=':
                    case '\\=':
                    case '^=':
                    case '**=':
                    case '.*=':
                    case './=':
                    case '.\\=':
                    case '.^=':
                    case '.**=':
                    case '&=':
                    case '|=':
                        return this.Unparse(tree.left) + tree.type + this.Unparse(tree.right);
                    case '()':
                        return '(' + this.Unparse(tree.right) + ')';
                    case '!':
                    case '~':
                        return tree.type + this.Unparse(tree.right);
                    case '+_':
                        return '+' + this.Unparse(tree.right);
                    case '-_':
                        return '-' + this.Unparse(tree.right);
                    case '++_':
                        return '++' + this.Unparse(tree.right);
                    case '--_':
                        return '--' + this.Unparse(tree.right);
                    case ".'":
                    case "'":
                        return this.Unparse(tree.left) + tree.type;
                    case '_++':
                        return this.Unparse(tree.left) + '++';
                    case '_--':
                        return this.Unparse(tree.left) + '--';
                    case 'NAME':
                        return tree.id;
                    case 'ARG':
                        arglist = '';
                        for (let i = 0; i < tree.args.length; i++) {
                            arglist += this.Unparse(tree.args[i]) + ',';
                        }
                        return this.Unparse(tree.expr) + '(' + arglist.substring(0, arglist.length - 1) + ')';
                    case 'CmdWList':
                        arglist = '';
                        for (let i = 0; i < tree.args.length; i++) {
                            arglist += this.Unparse(tree.args[i]) + ' ';
                        }
                        return tree.id + arglist.substring(0, arglist.length - 1);
                    default:
                        return '<INVALID>';
                }
            }
        } catch (e) {
            return '<ERROR>';
        }
    }

    Evaluator(tree: any, local: boolean, fname: string): any {
        let aliasTreeName: string;
        if (this.debug) console.log('Evaluator(tree:' + JSON.stringify(tree, null, 2) + ',local:' + local + ',fname:' + fname + ')');
        if ('list' in tree) {
            const result = { list: new Array(tree.list.length) };
            for (let i = 0; i < tree.list.length; i++) {
                // Convert undefined name, defined in word-list command, to word-list command.
                // (Null length word-list command)
                if (
                    tree.list[i].type == 'NAME' &&
                    !(local && this.localTable[fname] && this.localTable[fname][tree.list[i].id]) &&
                    !(tree.list[i].id in this.nameTable) &&
                    commandsTable.indexOf(tree.list[i].id) >= 0
                ) {
                    tree.list[i].type = 'CmdWList';
                    tree.list[i]['args'] = [];
                }
                result.list[i] = this.Evaluator(tree.list[i], local, fname);
            }
            return result;
        } else if (this.isNumber(tree) || this.isString(tree)) {
            // NUMBER or STRING
            return tree;
        } else if (this.isTensor(tree)) {
            // MATRIX
            return this.evaluateTensor(tree, this, fname);
        } else if (this.isRange(tree)) {
            // RANGE
            return this.expandRange(
                this.Evaluator(tree.start, local, fname),
                this.Evaluator(tree.stop, local, fname),
                tree.stride ? this.Evaluator(tree.stride, local, fname) : null,
            );
        } else {
            let result;
            switch (tree.type) {
                case '.+':
                case '+':
                case '.-':
                case '-':
                case '.*':
                case '*':
                case './':
                case '/':
                case '.÷':
                case '÷':
                case '.\\':
                case '\\':
                case '.^':
                case '^':
                case '.**':
                case '**':
                case '<':
                case '<=':
                case '==':
                case '>=':
                case '>':
                case '!=':
                case '~=':
                case '&':
                case '|':
                case '&&':
                case '||':
                    return this.opTable[tree.type](this.Evaluator(tree.left, local, fname), this.Evaluator(tree.right, local, fname));
                case '()':
                    return this.Evaluator(tree.right, local, fname);
                case '+_':
                case '-_':
                    return this.opTable[tree.type](this.Evaluator(tree.right, local, fname));
                case '++_':
                case '--_':
                    return this.opTable[tree.type](this.Evaluator(tree.right, local, fname));
                case ".'":
                case "'":
                case '_++':
                case '_--':
                    return this.opTable[tree.type](this.Evaluator(tree.left, local, fname));
                case '=':
                case '+=':
                case '-=':
                case '*=':
                case '/=':
                case '\\=':
                case '^=':
                case '**=':
                case '.*=':
                case './=':
                case '.\\=':
                case '.^=':
                case '.**=':
                case '&=':
                case '|=':
                    const op = tree.type.substring(0, tree.type.length - 1);
                    let left;
                    let id;
                    let args;
                    if (tree.left.type == 'NAME') {
                        left = tree.left;
                        id = tree.left.id;
                        aliasTreeName = this.aliasName(tree.left.id);
                        args = [];
                    } else {
                        // (tree.left.type=='ARG') && (tree.left.expr.type=='NAME')
                        left = tree.left.expr;
                        id = tree.left.expr.id;
                        aliasTreeName = this.aliasName(tree.left.expr.id);
                        args = tree.left.args;
                    }
                    if (aliasTreeName in this.baseFunctionTable) {
                        throw new Error('assign to reserved name: ' + aliasTreeName);
                    } else {
                        if (args.length == 0) {
                            // Name definition
                            try {
                                this.nameTable[id] = { args: [], expr: this.Evaluator(tree.right, false, fname) };
                                return this.nodeOp('=', left, this.nameTable[id].expr);
                            } catch (e) {
                                this.nameTable[id] = { args: [], expr: tree.right };
                                throw e;
                            }
                        } else {
                            if (this.nameTable[id] && this.isTensor(this.nameTable[id].expr)) {
                                // Matrix indexing refer
                                console.log('matrix indexing refer');
                                return tree; /** */
                            } else {
                                // Function definition
                                // Test if args is a list of NAME
                                let pass = true;
                                for (let i = 0; i < args.length; i++) {
                                    pass = pass && args[i].type == 'NAME';
                                    if (!pass) throw new Error('invalid arguments in function definition');
                                }
                                this.nameTable[id] = { args: args, expr: tree.right };
                                return tree;
                            }
                        }
                    }
                    break;
                case 'NAME':
                    if (local && this.localTable[fname] && this.localTable[fname][tree.id]) {
                        // Defined in localTable
                        return this.localTable[fname][tree.id];
                    } else if (tree.id in this.nameTable) {
                        // Defined in nameTable
                        if (this.nameTable[tree.id].args.length == 0) {
                            // Defined as name
                            return this.Evaluator(this.nameTable[tree.id].expr, false, fname);
                        } else {
                            // Defined as function name
                            throw new Error('calling ' + tree.id + ' function without arguments list');
                        }
                    }
                case 'ARG':
                    if (tree.expr.type == 'NAME') {
                        // Matrix indexing or function call
                        aliasTreeName = this.aliasName(tree.expr.id);
                        const argumentsList: any[] = [];
                        if (aliasTreeName in this.baseFunctionTable) {
                            // Is base function
                            if (typeof this.baseFunctionTable[aliasTreeName]['mapper'] !== 'undefined') {
                                // arguments evaluated
                                for (let i = 0; i < tree.args.length; i++) {
                                    // Evaluate arguments list
                                    argumentsList[i] = this.Evaluator(tree.args[i], local, fname);
                                }
                                if (this.baseFunctionTable[aliasTreeName].mapper && argumentsList.length != 1) {
                                    // Error if mapper and #arguments!=1 (Invalid call)
                                    throw new Error('Invalid call to ' + aliasTreeName + '.');
                                }
                                if (argumentsList.length == 1 && 'array' in argumentsList[0] && this.baseFunctionTable[aliasTreeName].mapper) {
                                    // Test if is mapper
                                    return this.mapTensor(argumentsList[0], this.baseFunctionTable[aliasTreeName].func);
                                } else {
                                    return this.baseFunctionTable[aliasTreeName].func(...argumentsList);
                                }
                            } else {
                                // arguments selectively evaluated
                                for (let i = 0; i < tree.args.length; i++) {
                                    // Evaluate arguments list selectively
                                    argumentsList[i] = (this.baseFunctionTable[aliasTreeName] as any).ev[i]
                                        ? this.Evaluator(tree.args[i], local, fname)
                                        : tree.args[i];
                                }
                                return this.baseFunctionTable[aliasTreeName].func(...argumentsList, this);
                            }
                        } else if (local && this.localTable[fname] && this.localTable[fname][tree.expr.id]) {
                            // Defined in localTable ****
                            return this.localTable[fname][tree.expr.id];
                        } else if (tree.expr.id in this.nameTable) {
                            // Defined in nameTable
                            if (this.nameTable[tree.expr.id].args.length == 0) {
                                // if is defined name
                                const temp = this.Evaluator(this.nameTable[tree.expr.id].expr, false, fname);
                                if (tree.args.length == 0) {
                                    // Defined name
                                    return temp;
                                } else if (this.isTensor(temp)) {
                                    // Defined matrix indexing
                                    for (let i = 0; i < tree.args.length; i++) {
                                        // Evaluate index list
                                        argumentsList[i] = this.Evaluator(tree.args[i], local, fname);
                                    }
                                    return this.subTensor(temp, tree.expr.id, argumentsList);
                                } else {
                                    throw new Error('invalid matrix indexing or function arguments');
                                }
                            } else {
                                // Else is defined function
                                if (this.nameTable[tree.expr.id].args.length != tree.args.length) {
                                    throw new Error('invalid number of arguments in function ' + tree.expr.id);
                                }
                                this.localTable[tree.expr.id] = {}; // create localTable entry
                                for (let i = 0; i < tree.args.length; i++) {
                                    // Evaluate defined function arguments list
                                    this.localTable[tree.expr.id][this.nameTable[tree.expr.id].args[i].id] = this.Evaluator(
                                        tree.args[i],
                                        true,
                                        fname,
                                    );
                                }
                                const temp = this.Evaluator(this.nameTable[tree.expr.id].expr, true, tree.expr.id);
                                delete this.localTable[tree.expr.id]; // delete localTable entry
                                return temp;
                            }
                        } else {
                            throw new Error("'" + tree.id + "' undefined");
                        }
                    } else {
                        // literal indexing, ex: [1,2;3,4](1,2)
                        console.log('literal indexing');
                        return tree; /** */
                    }
                case 'CmdWList':
                    switch (tree.id) {
                        case 'help':
                            this.exitStatus = Evaluator.response.INFO;
                            // this.exitMessage = helpCommand(tree.args);
                            break;
                        case 'doc':
                            this.exitStatus = Evaluator.response.INFO;
                            // this.exitMessage = docCommand(tree.args);
                            break;
                    }
                    return tree;
                default:
                    throw new Error("evaluating undefined type '" + tree.type + "'");
            }
        }
    }

    Evaluate(tree: any): any {
        try {
            this.exitStatus = Evaluator.response.OK;
            const result = this.Evaluator(tree, false, '');
            const assign_op = ['=', '+=', '.+=', '-=', '.-=', '*=', '/=', '\\=', '^=', '.*=', './=', '.\\=', '.^=', '.**=', '|=', '&='];
            if (assign_op.indexOf(result.type) == -1) {
                this.nameTable['ans'] = { args: [], expr: result };
            }
            if (this.exitStatus >= 0) {
                // not (WARNING or INFO)
                this.exitMessage = '';
            }
            return result;
        } catch (e) {
            this.exitStatus = Evaluator.response.EVAL_ERROR;
            throw e;
        }
    }

    unparserML(tree: any): string {
        try {
            if (tree === undefined) {
                return '';
            }
            if ('list' in tree) {
                let list = '<mtable>';
                for (let i = 0; i < tree.list.length; i++) {
                    list += '<mtr><mtd>' + this.unparserML(tree.list[i]) + '</mtd></mtr>';
                }
                return list + '</mtable>';
            } else if (this.isNumber(tree)) {
                // NUMBER
                return this.unparseNumberML(tree);
            } else if (this.isString(tree)) {
                // STRING
                return this.unparseStringML(tree);
            } else if (this.isTensor(tree)) {
                // MATRIX
                return this.unparseTensorML(tree, this);
            } else if (this.isRange(tree)) {
                // RANGE
                if (tree.start && tree.stop) {
                    if (tree.stride) {
                        return this.unparserML(tree.start) + '<mo>:</mo>' + this.unparserML(tree.stride) + '<mo>:</mo>' + this.unparserML(tree.stop);
                    } else {
                        return this.unparserML(tree.start) + '<mo>:</mo>' + this.unparserML(tree.stop);
                    }
                } else {
                    return '<mo>:</mo>';
                }
            } else {
                let arglist: string;
                switch (tree.type) {
                    case ':':
                    case '!':
                    case '~':
                        return '<mo>' + tree.type + '</mo>';
                    case '.+':
                    case '+':
                    case '.-':
                    case '-':
                    case '.*':
                    case './':
                    case '.÷':
                    case '÷':
                    case '.\\':
                    case '\\':
                    case '.^':
                    case '.**':
                    case '<':
                    case '>':
                    case '==':
                    case '&':
                    case '|':
                    case '&&':
                    case '||':
                    case '=':
                    case '+=':
                    case '-=':
                    case '*=':
                    case '/=':
                    case '\\=':
                    case '^=':
                    case '**=':
                    case '.*=':
                    case './=':
                    case '.\\=':
                    case '.^=':
                    case '.**=':
                    case '&=':
                    case '|=':
                        return this.unparserML(tree.left) + '<mo>' + tree.type + '</mo>' + this.unparserML(tree.right);
                    case '<=':
                        return this.unparserML(tree.left) + '<mo>&le;</mo>' + this.unparserML(tree.right);
                    case '>=':
                        return this.unparserML(tree.left) + '<mo>&ge;</mo>' + this.unparserML(tree.right);
                    case '!=':
                    case '~=':
                        return this.unparserML(tree.left) + '<mo>&ne;</mo>' + this.unparserML(tree.right);
                    case '()':
                        return '<mo>(</mo>' + this.unparserML(tree.right) + '<mo>)</mo>';
                    case '*':
                        return this.unparserML(tree.left) + '<mo>&times;</mo>' + this.unparserML(tree.right);
                    case '/':
                        return '<mfrac><mrow>' + this.unparserML(tree.left) + '</mrow><mrow>' + this.unparserML(tree.right) + '</mrow></mfrac>';
                    case '**':
                    case '^':
                        return '<msup><mrow>' + this.unparserML(tree.left) + '</mrow><mrow>' + this.unparserML(tree.right) + '</mrow></msup>';
                    case ':':
                    case '!':
                    case '~':
                        return '<mo>' + tree.type + '</mo>' + this.unparserML(tree.right);
                    case '+_':
                        return '<mo>+</mo>' + this.unparserML(tree.right);
                    case '-_':
                        return '<mo>-</mo>' + this.unparserML(tree.right);
                    case '++_':
                        return '<mo>++</mo>' + this.unparserML(tree.right);
                    case '--_':
                        return '<mo>--</mo>' + this.unparserML(tree.right);
                    case '_++':
                        return this.unparserML(tree.right) + '<mo>++</mo>';
                    case '_--':
                        return this.unparserML(tree.right) + '<mo>--</mo>';
                    case ".'":
                        return '<msup><mrow>' + this.unparserML(tree.left) + '</mrow><mrow><mi>T</mi></mrow></msup>';
                    case "'":
                        return '<msup><mrow>' + this.unparserML(tree.left) + '</mrow><mrow><mi>H</mi></mrow></msup>';
                    case 'NAME':
                        return '<mi>' + substGreek(tree.id) + '</mi>';
                    case 'ARG':
                        if (tree.args.length == 0) {
                            return this.unparserML(tree.expr) + '<mrow><mo>(</mo><mo>)</mo></mrow>';
                        } else {
                            arglist = '';
                            for (let i = 0; i < tree.args.length; i++) {
                                arglist += this.unparserML(tree.args[i]) + '<mo>,</mo>';
                            }
                            arglist = arglist.substring(0, arglist.length - 10);
                            if (tree.expr.type == 'NAME') {
                                const aliasTreeName = this.aliasName(tree.expr.id);
                                switch (aliasTreeName) {
                                    case 'abs':
                                        return '<mrow><mo>|</mo>' + this.unparserML(tree.args[0]) + '<mo>|</mo></mrow>';
                                    case 'conj':
                                        return '<mover><mrow>' + this.unparserML(tree.args[0]) + '</mrow><mo>&OverBar;</mo></mover>';
                                    case 'sqrt':
                                        return '<msqrt><mrow>' + this.unparserML(tree.args[0]) + '</mrow></msqrt>';
                                    case 'root':
                                        return (
                                            '<mroot><mrow>' +
                                            this.unparserML(tree.args[0]) +
                                            '</mrow><mrow>' +
                                            this.unparserML(tree.args[1]) +
                                            '</mrow></mroot>'
                                        );
                                    case 'exp':
                                        return '<msup><mi>e</mi><mrow>' + this.unparserML(tree.args[0]) + '</mrow></msup>';
                                    case 'log':
                                        return (
                                            '<msub><mi>log</mi><mrow>' +
                                            this.unparserML(tree.args[0]) +
                                            '</mrow></msub><mrow>' +
                                            this.unparserML(tree.args[1]) +
                                            '</mrow>'
                                        );
                                    case 'sum':
                                        return (
                                            '<mrow><munderover><mo>&#x2211;</mo><mrow>' +
                                            this.unparserML(tree.args[0]) +
                                            '<mo>=</mo>' +
                                            this.unparserML(tree.args[1]) +
                                            '</mrow>' +
                                            this.unparserML(tree.args[2]) +
                                            '</munderover></mrow>' +
                                            this.unparserML(tree.args[3])
                                        );
                                    case 'prod':
                                        return (
                                            '<mrow><munderover><mo>&#x220F;</mo><mrow>' +
                                            this.unparserML(tree.args[0]) +
                                            '<mo>=</mo>' +
                                            this.unparserML(tree.args[1]) +
                                            '</mrow>' +
                                            this.unparserML(tree.args[2]) +
                                            '</munderover></mrow>' +
                                            this.unparserML(tree.args[3])
                                        );
                                    case 'gamma':
                                        return '<mi>&Gamma;</mi><mrow><mo>(</mo>' + arglist + '<mo>)</mo></mrow>';
                                    case 'factorial':
                                        return '<mrow><mo>(</mo>' + this.unparserML(tree.args[0]) + '<mo>)</mo></mrow><mo>!</mo>';
                                    case 'binomial':
                                        return (
                                            '<mrow><mo>(</mo><mtable><mtr><mtd>' +
                                            this.unparserML(tree.args[0]) +
                                            '</mtd></mtr><mtr><mtd>' +
                                            this.unparserML(tree.args[1]) +
                                            '</mtd></mtr></mtable><mo>)</mo></mrow>'
                                        );
                                    default:
                                        return '<mi>' + substGreek(tree.expr.id) + '</mi><mrow><mo>(</mo>' + arglist + '<mo>)</mo></mrow>';
                                }
                            } else {
                                return '<mi>' + this.unparserML(tree.expr) + '</mi><mrow><mo>(</mo>' + arglist + '<mo>)</mo></mrow>';
                            }
                        }
                    case 'CmdWList':
                        arglist = ' ';
                        for (let i = 0; i < tree.args.length; i++) {
                            arglist += this.Unparse(tree.args[i]) + ' ';
                        }
                        return '<mtext>' + tree.id + ' ' + arglist.substring(0, arglist.length - 1) + '</mtext>';
                    default:
                        return '<mi>invalid</mi>';
                }
            }
        } catch (e) {
            if (this.debug) {
                throw e;
            } else {
                return '<mi>error</mi>';
            }
        }
    }

    UnparseML(tree: any): string {
        let result: string = this.unparserML(tree);
        result = result.replace(/\<mo\>\(\<\/mo\>\<mi\>error\<\/mi\><\mi\>error\<\/mi\>\<mi\>i\<\/mi\>\<mo\>\)\<\/mo\>/gi, '<mi>error</mi>');
        return "<math xmlns = 'http://www.w3.org/1998/Math/MathML' display='block'>" + result + '</math>';
    }
}
