Object.defineProperty(exports, "__esModule", { value: true });
const OperationTypes = require("./OperationTypes");
const babelPluginHelpers_1 = require("./babelPluginHelpers");
const jsonToAst = require("json-to-ast");
const adjustColumnForEscapeSequences_1 = require("./adjustColumnForEscapeSequences");
const getJSONPathOffset_1 = require("./getJSONPathOffset");
const CallExpression_1 = require("./operations/CallExpression");
const MemberExpression_1 = require("./operations/MemberExpression");
const ObjectExpression_1 = require("./operations/ObjectExpression");
const AssignmentExpression_1 = require("./operations/AssignmentExpression");
const traverseStringConcat_1 = require("./traverseStringConcat");
const MemoValueNames = require("./MemoValueNames");
const traverseDomOrigin_1 = require("./traverseDomOrigin");
const config_1 = require("./config");
const FunctionNames = require("./FunctionNames");
const sortBy = require("lodash/sortBy");
function identifyTraverseFunction(operationLog, charIndex) {
    return {
        operationLog: operationLog.args.value,
        charIndex
    };
}
let t, babylon;
// This file is also imported into helperFunctions, i.e. FE code that can't load
// Babel dependencies
function initForBabel(babelTypes, _babylon) {
    t = babelTypes;
    babylon = _babylon;
    babelPluginHelpers_1.initForBabel(babelTypes);
}
exports.initForBabel = initForBabel;
function createNode(args, astArgs = null) { }
const operations = {
    memberExpression: MemberExpression_1.MemberExpression,
    binaryExpression: {
        visitor(path) {
            if (!["+", "-", "/", "*"].includes(path.node.operator)) {
                return;
            }
            return this.createNode({
                left: [path.node.left, babelPluginHelpers_1.getLastOperationTrackingResultCall()],
                right: [path.node.right, babelPluginHelpers_1.getLastOperationTrackingResultCall()]
            }, { operator: babelPluginHelpers_1.ignoredStringLiteral(path.node.operator) }, path.node.loc);
        },
        traverse(operationLog, charIndex, options) {
            const { operator } = operationLog.astArgs;
            const { left, right } = operationLog.args;
            const leftIsNumericLiteral = left.operation === "numericLiteral";
            const rightIsNumericLiteral = right.operation === "numericLiteral";
            const numericLiteralCount = (leftIsNumericLiteral ? 1 : 0) + (rightIsNumericLiteral ? 1 : 0);
            if (operator == "+") {
                if (typeof left.result.type === "string" &&
                    typeof right.result.type === "string") {
                    return traverseStringConcat_1.default(left, right, charIndex);
                }
                else {
                    console.log("todo");
                }
            }
            else {
                console.log("todo binexp operator " + operator);
            }
            if (options && options.optimistic && numericLiteralCount === 1) {
                // We can't be quite sure, but probably the user cares about the
                // more complex value, not the simple hard coded value
                const complexOperation = leftIsNumericLiteral ? right : left;
                return {
                    isOptimistic: true,
                    charIndex,
                    operationLog: complexOperation
                };
            }
            throw "aaa";
        },
        exec: (args, astArgs, ctx) => {
            var { left, right } = args;
            var ret;
            left = left[0];
            right = right[0];
            var { operator } = astArgs;
            if (operator === "+") {
                ret = left + right;
            }
            else if (operator === "-") {
                ret = left - right;
            }
            else if (operator === "*") {
                ret = left * right;
            }
            else if (operator === "/") {
                ret = left / right;
            }
            else {
                throw Error("unknown bin exp operator: " + operator);
            }
            return ret;
        }
    },
    conditionalExpression: {
        exec: (args, astArgs, ctx) => {
            return args.result[0];
        },
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.result,
                charIndex
            };
        },
        visitor(path) {
            var saveTestValue = babelPluginHelpers_1.createSetMemoValue(MemoValueNames.lastConditionalExpressionTest, path.node.test, babelPluginHelpers_1.getLastOperationTrackingResultCall());
            var saveConsequentValue = babelPluginHelpers_1.createSetMemoValue(MemoValueNames.lastConditionalExpressionResult, path.node.consequent, babelPluginHelpers_1.getLastOperationTrackingResultCall());
            var saveAlernativeValue = babelPluginHelpers_1.createSetMemoValue(MemoValueNames.lastConditionalExpressionResult, path.node.alternate, babelPluginHelpers_1.getLastOperationTrackingResultCall());
            var operation = this.createNode({
                test: [
                    babelPluginHelpers_1.createGetMemoValue(MemoValueNames.lastConditionalExpressionTest),
                    babelPluginHelpers_1.createGetMemoTrackingValue(MemoValueNames.lastConditionalExpressionTest)
                ],
                result: [
                    babelPluginHelpers_1.ignoreNode(t.conditionalExpression(babelPluginHelpers_1.createGetMemoValue(MemoValueNames.lastConditionalExpressionTest), saveConsequentValue, saveAlernativeValue)),
                    babelPluginHelpers_1.createGetMemoTrackingValue(MemoValueNames.lastConditionalExpressionResult)
                ]
            }, {}, path.node.loc);
            path.replaceWith(t.sequenceExpression([saveTestValue, operation]));
        }
    },
    stringReplacement: {},
    callExpression: CallExpression_1.default,
    newExpression: {
        visitor(path) {
            return operations.callExpression.visitor(path, true);
        }
    },
    objectProperty: {
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.propertyValue,
                charIndex: charIndex
            };
        }
    },
    classDeclaration: {
        visitor(path) {
            // doesn't seem to have binding.kind, so can't just ignore it in
            // safelyGetVariableTrackingValue -> make sure tracking var exists
            path.insertAfter(babelPluginHelpers_1.skipPath(t.variableDeclaration("var", [
                t.variableDeclarator(t.identifier(babelPluginHelpers_1.getTrackingVarName(path.node.id.name)))
            ])));
        }
    },
    objectExpression: ObjectExpression_1.default,
    stringLiteral: {
        canInferResult: true,
        shorthand: {
            fnName: "__str",
            getExec: doOperation => {
                return (value, loc) => {
                    return doOperation([value], undefined, loc);
                };
            },
            visitor: (opArgs, astArgs, locAstNode) => {
                const valueArg = opArgs[0];
                return babelPluginHelpers_1.ignoredCallExpression("__str", [
                    babelPluginHelpers_1.ignoredArrayExpression(valueArg),
                    locAstNode
                ]);
            }
        },
        argNames: ["value"],
        visitor(path) {
            if (path.parent.type === "ObjectProperty") {
                return;
            }
            const value = path.node.value;
            // Store on loc so it can be looked up later
            path.node.loc.value = value;
            return babelPluginHelpers_1.skipPath(this.createNode([[babelPluginHelpers_1.ignoredStringLiteral(value)]], {}, path.node.loc));
        },
        exec: (args, astArgs, ctx) => {
            return args[0][0];
        }
    },
    numericLiteral: {
        shorthand: {
            fnName: "__num",
            getExec: doOperation => {
                return (value, loc) => {
                    return doOperation([value], undefined, loc);
                };
            },
            visitor: (opArgs, astArgs, locAstNode) => {
                const valueArg = opArgs[0];
                return babelPluginHelpers_1.ignoredCallExpression("__num", [
                    babelPluginHelpers_1.ignoredArrayExpression(valueArg),
                    locAstNode
                ]);
            }
        },
        argNames: ["value"],
        visitor(path) {
            if (path.parent.type === "ObjectProperty") {
                return;
            }
            const value = path.node.value;
            return babelPluginHelpers_1.skipPath(this.createNode([[babelPluginHelpers_1.ignoredNumericLiteral(value)]], null, path.node.loc));
        },
        exec: (args, astArgs, ctx) => {
            return args[0][0];
        }
    },
    templateLiteral: {
        exec: (args, astArgs, ctx, logData) => {
            logData.extraArgs = {};
            logData.runtimeArgs = {};
            const expressionValues = ctx.getCurrentTemplateLiteralTrackingValues();
            expressionValues.forEach((expressionValue, i) => {
                logData.extraArgs["expression" + i] = [
                    null,
                    expressionValue.trackingValue
                ];
                logData.runtimeArgs["expression" + i + "Length"] =
                    expressionValue.valueLength;
            });
            return args.literal[0];
        },
        visitor(path) {
            let literalParts = [...path.node.expressions, ...path.node.quasis];
            literalParts = sortBy(literalParts, p => {
                const start = p.loc.start;
                return start.line * 10000 + start.column;
            });
            const structure = literalParts.map(part => {
                if (part.type === "TemplateElement") {
                    return babelPluginHelpers_1.ignoredObjectExpression({
                        type: babelPluginHelpers_1.ignoredStringLiteral("quasi"),
                        // cooked e.g. has escape sequences resolved
                        length: babelPluginHelpers_1.ignoredNumericLiteral(part.value.cooked.length)
                    });
                }
                else {
                    return babelPluginHelpers_1.ignoredObjectExpression({
                        type: babelPluginHelpers_1.ignoredStringLiteral("expression")
                    });
                }
            });
            const astArgs = {
                structureParts: babelPluginHelpers_1.ignoredArrayExpression(structure)
            };
            path.node.expressions = path.node.expressions.map(expression => {
                return babelPluginHelpers_1.ignoredCallExpression(FunctionNames.saveTemplateLiteralExpressionTrackingValue, [expression]);
            });
            const args = {
                // not sure why i need ignored array exp here?
                literal: babelPluginHelpers_1.ignoredArrayExpression([babelPluginHelpers_1.ignoreNode(path.node), null])
            };
            return t.sequenceExpression([
                babelPluginHelpers_1.ignoredCallExpression(FunctionNames.enterTemplateLiteral, []),
                this.createNode(args, astArgs, path.node.loc)
            ]);
        },
        traverse(operationLog, charIndex) {
            const structureParts = operationLog.astArgs.structureParts;
            let indexInString = 0;
            let expressionIndex = 0;
            for (var partIndex = 0; partIndex < structureParts.length; partIndex++) {
                const structurePart = structureParts[partIndex];
                let indexInStringAfter = indexInString;
                if (structurePart.type === "quasi") {
                    indexInStringAfter += structurePart.length;
                    if (indexInStringAfter > charIndex) {
                        return {
                            charIndex: charIndex,
                            operationLog: null
                        };
                    }
                }
                else {
                    const expressionTrackingValue = operationLog.extraArgs["expression" + expressionIndex];
                    const expressionValueLength = operationLog.runtimeArgs["expression" + expressionIndex + "Length"];
                    expressionIndex++;
                    indexInStringAfter += expressionValueLength;
                    if (indexInStringAfter > charIndex) {
                        return {
                            operationLog: expressionTrackingValue,
                            charIndex: charIndex - indexInString
                        };
                    }
                }
                indexInString = indexInStringAfter;
            }
            return {
                operationLog: null,
                charIndex
            };
        }
    },
    unaryExpression: {
        exec: (args, astArgs, ctx) => {
            if (astArgs.operator === "-") {
                return -args.argument[0];
            }
            else if (astArgs.operator === "delete") {
                const obj = args.object[0];
                const propName = args.propName[0];
                const ret = delete obj[propName];
                if (typeof obj === "object") {
                    ctx.trackObjectPropertyAssignment(obj, propName, null, null);
                }
                return ret;
            }
            else {
                throw Error("unknown unary expression operator");
            }
        },
        visitor(path) {
            let args;
            if (path.node.operator === "-") {
                args = {
                    argument: [path.node.argument, babelPluginHelpers_1.getLastOperationTrackingResultCall()]
                };
            }
            else if (path.node.operator === "delete") {
                let propName;
                const arg = path.node.argument;
                let object;
                if (arg.type === "MemberExpression") {
                    object = arg.object;
                    if (arg.computed === true) {
                        propName = arg.property;
                    }
                    else {
                        propName = babelPluginHelpers_1.addLoc(this.t.stringLiteral(arg.property.name), arg.property.loc);
                    }
                }
                else if (arg.type === "Identifier") {
                    // Deleting a global like this: delete someGlobal
                    propName = babelPluginHelpers_1.addLoc(babelPluginHelpers_1.ignoredStringLiteral(arg.name), arg.loc);
                    object = babelPluginHelpers_1.getGetGlobalCall();
                }
                else {
                    console.log("unknown delete argument type: " + arg.type);
                    return;
                }
                args = {
                    object: [object, null],
                    propName: [propName, null]
                };
            }
            else {
                return;
            }
            return this.createNode(args, {
                operator: babelPluginHelpers_1.ignoredStringLiteral(path.node.operator)
            }, path.node.loc);
        }
    },
    arrayExpression: {
        argNames: ["element"],
        argIsArray: [true],
        exec: (args, astArgs, ctx, logData) => {
            const [elementsArg] = args;
            let arr = [];
            elementsArg.forEach((el, i) => {
                const [value, trackingValue] = el;
                arr.push(value);
                const nameTrackingValue = ctx.createArrayIndexOperationLog(i, logData.loc);
                ctx.trackObjectPropertyAssignment(arr, i.toString(), trackingValue, nameTrackingValue);
            });
            return arr;
        },
        visitor(path) {
            const elements = [];
            path.node.elements.forEach(el => {
                if (el /* check for el because it can be null if array has empty elements like [1,,3] */ &&
                    el.type === "SpreadElement") {
                    elements.push(t.spreadElement(babelPluginHelpers_1.ignoredCallExpression(FunctionNames.expandArrayForSpreadElement, [
                        el.argument
                    ])));
                }
                else {
                    elements.push(babelPluginHelpers_1.ignoredArrayExpression([el, babelPluginHelpers_1.getLastOperationTrackingResultCall()]));
                }
            });
            return this.createNode([elements], null, path.node.loc);
        }
    },
    returnStatement: {
        argNames: ["returnValue"],
        canInferResult: function (args) {
            // return statement will always return returned value
            return !!args[0];
        },
        exec: (args, astArgs, ctx, logData) => {
            const [returnValueArg] = args;
            ctx.lastReturnStatementResult = [returnValueArg[0], logData.index];
            return returnValueArg[0];
        },
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.returnValue,
                charIndex: charIndex
            };
        },
        visitor(path) {
            path.node.argument = this.createNode([[path.node.argument, babelPluginHelpers_1.getLastOperationTrackingResultCall()]], {}, path.node.loc);
        }
    },
    identifier: {
        argNames: ["value"],
        canInferResult: function (args) {
            // identifier will always return same value as var value
            return !!args[0];
        },
        shorthand: {
            fnName: "__ident",
            getExec: doOperation => {
                return (value, loc) => {
                    return doOperation([value], undefined, loc);
                };
            },
            visitor: (opArgs, astArgs, locAstNode) => {
                if (astArgs && astArgs["isArguments"]) {
                    // __ident shorthand doesn't support astArgs
                    return null;
                }
                return babelPluginHelpers_1.ignoredCallExpression("__ident", [
                    babelPluginHelpers_1.ignoredArrayExpression(opArgs[0]),
                    locAstNode
                ]);
            }
        },
        exec: (args, astArgs, ctx, logData) => {
            const [valueArg, allFnArgTrackingValuesArg] = args;
            if (astArgs &&
                astArgs.isArguments &&
                !ctx.objectHasPropertyTrackingData(valueArg[0])) {
                if (allFnArgTrackingValuesArg[0]) {
                    allFnArgTrackingValuesArg[0].forEach((trackingValue, i) => {
                        ctx.trackObjectPropertyAssignment(valueArg[0], i, trackingValue, ctx.createArrayIndexOperationLog(i, logData.loc));
                    });
                }
                else {
                    if (config_1.VERIFY) {
                        console.log("no tracking values for arguments object");
                    }
                }
            }
            return valueArg[0];
        },
        traverse: identifyTraverseFunction,
        visitor(path) {
            if (shouldSkipIdentifier(path)) {
                return;
            }
            path.node.ignore = true;
            let node = path.node;
            let trackingIdentiferLookup = babelPluginHelpers_1.safelyGetVariableTrackingValue(path.node.name, path.scope);
            let astArgs = null;
            const args = [[node, trackingIdentiferLookup]];
            if (node.name === "arguments") {
                astArgs = { isArguments: babelPluginHelpers_1.ignoreNode(t.booleanLiteral(true)) };
                args.push([babelPluginHelpers_1.ignoredIdentifier("__allArgTV")]);
            }
            return babelPluginHelpers_1.skipPath(this.createNode(args, astArgs, path.node.loc));
        }
    },
    memexpAsLeftAssExp: {
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.extraArgs.propertyValue,
                charIndex
            };
        }
    },
    splitResult: {
        traverse(operationLog, charIndex) {
            const { string, separator: separatorArg } = operationLog.args;
            const originalString = string.result.primitive;
            const separator = (separatorArg && separatorArg.result.primitive) || "";
            console.log("TODO: actually capture the indices where the string came from at runtime");
            return {
                operationLog: operationLog.args.string,
                charIndex: originalString.indexOf(operationLog.result.primitive) + charIndex
            };
        }
    },
    matchResult: {
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.input,
                charIndex: operationLog.runtimeArgs.matchIndex + charIndex
            };
        }
    },
    execResult: {
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.string,
                charIndex: charIndex + operationLog.runtimeArgs.matchIndex
            };
        }
    },
    jsonParseResult: {
        traverse(operationLog, charIndex) {
            // This traversal method is inaccurate but still useful
            // Ideally we should probably have a JSON parser
            const valueReadFromJson = operationLog.result.primitive;
            const json = operationLog.args.json.result.primitive;
            const keyPath = operationLog.runtimeArgs.keyPath;
            if (operationLog.runtimeArgs.isPrimitive) {
                return {
                    operationLog: operationLog.args.json,
                    charIndex: charIndex + operationLog.runtimeArgs.charIndexAdjustment
                };
            }
            const ast = jsonToAst(json, { loc: true });
            const valueStart = getJSONPathOffset_1.getJSONPathOffset(json, ast, keyPath, operationLog.runtimeArgs.isKey);
            charIndex = adjustColumnForEscapeSequences_1.adjustColumnForEscapeSequences(json.slice(valueStart), charIndex);
            charIndex += valueStart;
            return {
                operationLog: operationLog.args.json,
                charIndex
            };
        }
    },
    arrayPattern: {
        traverse(operationLog, charIndex) {
            return {
                operationLog: operationLog.args.value,
                charIndex
            };
        }
    },
    arrayIndex: {},
    assignmentExpression: AssignmentExpression_1.default,
    objectAssignResult: {
        traverse: identifyTraverseFunction
    },
    arraySlice: { traverse: identifyTraverseFunction },
    arraySplice: { traverse: identifyTraverseFunction },
    arrayConcat: {
        traverse: identifyTraverseFunction
    },
    styleAssignment: {
        traverse: (operationLog, charIndex) => {
            const styleName = operationLog.args.styleName.result.primitive;
            const styleNamePrefix = '="';
            const styleNameText = styleNamePrefix + styleName + ": ";
            if (charIndex < styleNameText.length) {
                return {
                    operationLog: operationLog.args.styleName,
                    charIndex: Math.min(Math.max(charIndex - styleNamePrefix.length, 0), styleName.length)
                };
            }
            else {
                return {
                    operationLog: operationLog.args.styleValue,
                    charIndex: charIndex - styleNameText.length
                };
            }
        }
    },
    htmlAdapter: {
        traverse: (operationLog, charIndex) => {
            return {
                operationLog: operationLog.args.html,
                charIndex: traverseDomOrigin_1.traverseDomOrigin(operationLog.runtimeArgs, charIndex)
            };
        }
    }
};
function eachArgumentInObject(args, operationName, fn) {
    if (!args) {
        return;
    }
    const operation = operations[operationName];
    const isObjectExpression = operationName === OperationTypes.objectExpression;
    if (isObjectExpression) {
        // todo: this is an objexpression property not an obj expression itself, should be clarified
        fn(args.value, "value", newValue => {
            args.value = newValue;
        });
        fn(args.key, "key", newValue => (args.key = newValue));
    }
    else {
        Object.keys(args).forEach(key => {
            fn(args[key], key, newValue => (args[key] = newValue));
        });
    }
}
function eachArgument(operationLog, fn) {
    eachArgumentInObject(operationLog.args, operationLog.operation, fn);
    if (operationLog.extraArgs) {
        eachArgumentInObject(operationLog.extraArgs, operationLog.operation, fn);
    }
}
exports.eachArgument = eachArgument;
Object.keys(operations).forEach(opName => {
    const operation = operations[opName];
    if (!OperationTypes[opName]) {
        throw Error("No op type: " + opName);
    }
    Object.defineProperty(operation, "t", {
        get() {
            return t;
        }
    });
    Object.defineProperty(operation, "babylon", {
        get() {
            return babylon;
        }
    });
    operation.createNode = function (args, astArgs, loc = null) {
        const operation = babelPluginHelpers_1.createOperation(OperationTypes[opName], args, astArgs, loc, operations[opName].shorthand || null);
        return operation;
    };
    operation.getArgumentsArray = function (operationLog) {
        var ret = [];
        eachArgument(operationLog, (arg, argName, updateValue) => {
            ret.push({ arg: arg, argName });
        });
        return ret;
    };
});
exports.default = operations;
function shouldSkipIdentifier(path) {
    if ([
        "FunctionDeclaration",
        "ArrowFunctionExpression",
        "MemberExpression",
        "ObjectProperty",
        "CatchClause",
        "ForOfStatement",
        "ForInStatement",
        "IfStatement",
        "ForStatement",
        "FunctionExpression",
        "UpdateExpression",
        "LabeledStatement",
        "ContinueStatement",
        "BreakStatement",
        "ClassMethod",
        "ClassProperty",
        "ClassDeclaration",
        "ClassExpression",
        "AssignmentPattern",
        "ArrayPattern",
        "RestElement"
    ].includes(path.parent.type)) {
        return true;
    }
    if (path.parent.type === "UnaryExpression" &&
        path.parent.operator === "typeof") {
        return true;
    }
    if (babelPluginHelpers_1.isInLeftPartOfAssignmentExpression(path) ||
        babelPluginHelpers_1.isInIdOfVariableDeclarator(path)) {
        return true;
    }
    return false;
}
exports.shouldSkipIdentifier = shouldSkipIdentifier;
//# sourceMappingURL=operations.js.map