"use strict";
exports.__esModule = true;
var babylon_1 = require("babylon");
var b = require("babel-types");
var binaryOperation_1 = require("./binaryOperation");
function expressionToConstant(expression, options) {
    if (options === void 0) { options = {}; }
    var constant = true;
    function toConstant(expression) {
        if (!constant)
            return;
        if (b.isArrayExpression(expression)) {
            var result_1 = [];
            for (var i = 0; constant && i < expression.elements.length; i++) {
                var element = expression.elements[i];
                if (b.isSpreadElement(element)) {
                    var spread = toConstant(element.argument);
                    if (!(isSpreadable(spread) && constant)) {
                        constant = false;
                    }
                    else {
                        result_1.push.apply(result_1, spread);
                    }
                }
                else {
                    result_1.push(toConstant(element));
                }
            }
            return result_1;
        }
        if (b.isBinaryExpression(expression)) {
            var left = toConstant(expression.left);
            var right = toConstant(expression.right);
            return constant && binaryOperation_1["default"](expression.operator, left, right);
        }
        if (b.isBooleanLiteral(expression)) {
            return expression.value;
        }
        if (b.isCallExpression(expression)) {
            var args = [];
            for (var i = 0; constant && i < expression.arguments.length; i++) {
                var arg = expression.arguments[i];
                if (b.isSpreadElement(arg)) {
                    var spread = toConstant(arg.argument);
                    if (!(isSpreadable(spread) && constant)) {
                        constant = false;
                    }
                    else {
                        args.push.apply(args, spread);
                    }
                }
                else {
                    args.push(toConstant(arg));
                }
            }
            if (!constant)
                return;
            if (b.isMemberExpression(expression.callee)) {
                var object = toConstant(expression.callee.object);
                if (!object || !constant) {
                    constant = false;
                    return;
                }
                var member = expression.callee.computed
                    ? toConstant(expression.callee.property)
                    : b.isIdentifier(expression.callee.property)
                        ? expression.callee.property.name
                        : undefined;
                if (member === undefined && !expression.callee.computed) {
                    constant = false;
                }
                if (!constant)
                    return;
                if (canCallMethod(object, '' + member)) {
                    return object[member].apply(object, args);
                }
            }
            else {
                var callee = toConstant(expression.callee);
                if (!constant)
                    return;
                return callee.apply(null, args);
            }
        }
        if (b.isConditionalExpression(expression)) {
            var test = toConstant(expression.test);
            return test
                ? toConstant(expression.consequent)
                : toConstant(expression.alternate);
        }
        if (b.isIdentifier(expression)) {
            if (options.constants &&
                {}.hasOwnProperty.call(options.constants, expression.name)) {
                return options.constants[expression.name];
            }
        }
        if (b.isLogicalExpression(expression)) {
            var left = toConstant(expression.left);
            var right = toConstant(expression.right);
            if (constant && expression.operator === '&&') {
                return left && right;
            }
            if (constant && expression.operator === '||') {
                return left || right;
            }
        }
        if (b.isMemberExpression(expression)) {
            var object = toConstant(expression.object);
            if (!object || !constant) {
                constant = false;
                return;
            }
            var member = expression.computed
                ? toConstant(expression.property)
                : b.isIdentifier(expression.property)
                    ? expression.property.name
                    : undefined;
            if (member === undefined && !expression.computed) {
                constant = false;
            }
            if (!constant)
                return;
            if ({}.hasOwnProperty.call(object, '' + member) && member[0] !== '_') {
                return object[member];
            }
        }
        if (b.isNullLiteral(expression)) {
            return null;
        }
        if (b.isNumericLiteral(expression)) {
            return expression.value;
        }
        if (b.isObjectExpression(expression)) {
            var result_2 = {};
            for (var i = 0; constant && i < expression.properties.length; i++) {
                var property = expression.properties[i];
                if (b.isObjectProperty(property)) {
                    if (property.shorthand) {
                        constant = false;
                        return;
                    }
                    var key = property.computed
                        ? toConstant(property.key)
                        : b.isIdentifier(property.key)
                            ? property.key.name
                            : b.isStringLiteral(property.key)
                                ? property.key.value
                                : undefined;
                    if (!key || key[0] === '_') {
                        constant = false;
                    }
                    if (!constant)
                        return;
                    var value = toConstant(property.value);
                    if (!constant)
                        return;
                    result_2[key] = value;
                }
                else if (b.isObjectMethod(property)) {
                    constant = false;
                }
                else if (b.isSpreadProperty(property)) {
                    var argument = toConstant(property.argument);
                    if (!argument)
                        constant = false;
                    if (!constant)
                        return;
                    Object.assign(result_2, argument);
                }
            }
            return result_2;
        }
        if (b.isParenthesizedExpression(expression)) {
            return toConstant(expression.expression);
        }
        if (b.isRegExpLiteral(expression)) {
            return new RegExp(expression.pattern, expression.flags);
        }
        if (b.isSequenceExpression(expression)) {
            for (var i = 0; i < expression.expressions.length - 1 && constant; i++) {
                toConstant(expression.expressions[i]);
            }
            return toConstant(expression.expressions[expression.expressions.length - 1]);
        }
        if (b.isStringLiteral(expression)) {
            return expression.value;
        }
        // TODO: TaggedTemplateExpression
        if (b.isTemplateLiteral(expression)) {
            var result_3 = '';
            for (var i = 0; i < expression.quasis.length; i++) {
                var quasi = expression.quasis[i];
                result_3 += quasi.value.cooked;
                if (i < expression.expressions.length) {
                    result_3 += '' + toConstant(expression.expressions[i]);
                }
            }
            return result_3;
        }
        if (b.isUnaryExpression(expression)) {
            var argument = toConstant(expression.argument);
            if (!constant) {
                return;
            }
            switch (expression.operator) {
                case '-':
                    return -argument;
                case '+':
                    return +argument;
                case '!':
                    return !argument;
                case '~':
                    return ~argument;
                case 'typeof':
                    return typeof argument;
                case 'void':
                    return void argument;
            }
        }
        constant = false;
    }
    var result = toConstant(expression);
    return constant ? { constant: true, result: result } : { constant: false };
}
exports.expressionToConstant = expressionToConstant;
function isSpreadable(value) {
    return (typeof value === 'string' ||
        Array.isArray(value) ||
        (typeof Set !== 'undefined' && value instanceof Set) ||
        (typeof Map !== 'undefined' && value instanceof Map));
}
function shallowEqual(a, b) {
    if (a === b)
        return true;
    if (a && b && typeof a === 'object' && typeof b === 'object') {
        for (var key in a) {
            if (a[key] !== b[key]) {
                return false;
            }
        }
        for (var key in b) {
            if (a[key] !== b[key]) {
                return false;
            }
        }
        return true;
    }
    return false;
}
function canCallMethod(object, member) {
    switch (typeof object) {
        case 'boolean':
            switch (member) {
                case 'toString':
                    return true;
                default:
                    return false;
            }
        case 'number':
            switch (member) {
                case 'toExponential':
                case 'toFixed':
                case 'toPrecision':
                case 'toString':
                    return true;
                default:
                    return false;
            }
        case 'string':
            switch (member) {
                case 'charAt':
                case 'charCodeAt':
                case 'codePointAt':
                case 'concat':
                case 'endsWith':
                case 'includes':
                case 'indexOf':
                case 'lastIndexOf':
                case 'match':
                case 'normalize':
                case 'padEnd':
                case 'padStart':
                case 'repeat':
                case 'replace':
                case 'search':
                case 'slice':
                case 'split':
                case 'startsWith':
                case 'substr':
                case 'substring':
                case 'toLowerCase':
                case 'toUpperCase':
                case 'trim':
                    return true;
                default:
                    return false;
            }
        default:
            if (object instanceof RegExp) {
                switch (member) {
                    case 'test':
                    case 'exec':
                        return true;
                    default:
                        return false;
                }
            }
            return {}.hasOwnProperty.call(object, member) && member[0] !== '_';
    }
}
var EMPTY_OBJECT = {};
var lastSrc = '';
var lastConstants = EMPTY_OBJECT;
var lastOptions = EMPTY_OBJECT;
var lastResult = null;
var lastWasConstant = false;
function isConstant(src, constants, options) {
    if (constants === void 0) { constants = EMPTY_OBJECT; }
    if (options === void 0) { options = EMPTY_OBJECT; }
    if (lastSrc === src &&
        shallowEqual(lastConstants, constants) &&
        shallowEqual(lastOptions, options)) {
        return lastWasConstant;
    }
    lastSrc = src;
    lastConstants = constants;
    var ast;
    try {
        ast = babylon_1.parseExpression(src, options);
    }
    catch (ex) {
        return (lastWasConstant = false);
    }
    var _a = expressionToConstant(ast, { constants: constants }), result = _a.result, constant = _a.constant;
    lastResult = result;
    return (lastWasConstant = constant);
}
exports.isConstant = isConstant;
function toConstant(src, constants, options) {
    if (constants === void 0) { constants = EMPTY_OBJECT; }
    if (options === void 0) { options = EMPTY_OBJECT; }
    if (!isConstant(src, constants, options)) {
        throw new Error(JSON.stringify(src) + ' is not constant.');
    }
    return lastResult;
}
exports.toConstant = toConstant;
exports["default"] = isConstant;
module.exports = isConstant;
module.exports["default"] = isConstant;
module.exports.expressionToConstant = expressionToConstant;
module.exports.isConstant = isConstant;
module.exports.toConstant = toConstant;
