import * as t from "@babel/types";
import { getDynamicBindings } from "@superblocksteam/shared";
import { extractJsEvaluationPairs } from "../binding-extraction/js-identifiers.js";
import { getIdentiferName } from "../parsing/util.js";
function replaceStringSegment(str, newStr, range) {
    const { start, end } = range;
    return str.slice(0, start) + newStr + str.slice(end);
}
export function renameEntityInBindings(options) {
    const { stringSegments, jsSnippets } = getDynamicBindings(options.bindingString);
    const { oldName, newName, parentBinding } = options;
    const result = jsSnippets.map((snippet, index) => {
        if (snippet.length === 0) {
            return stringSegments[index];
        }
        const pairs = extractJsEvaluationPairs(snippet);
        for (const pair of pairs) {
            const dottedBindingName = parentBinding
                ? `${parentBinding}.${oldName}`
                : oldName;
            if (pair.binding.startsWith(`${dottedBindingName}.`) ||
                pair.binding.match(new RegExp(`^${parentBinding ?? oldName}\\s*$`))) {
                const newBinding = pair.binding.replace(oldName, newName);
                snippet = replaceStringSegment(snippet, newBinding, pair.range);
            }
        }
        return `{{${snippet}}}`;
    });
    return result.join("");
}
export function renameEntityInJavascript({ oldName, newName, nodePath, parentBinding, checkFunctionBinding = false, }) {
    const functionVisitor = checkFunctionBinding
        ? {
            Function(path) {
                const paramBindings = path.scope.getAllBindingsOfKind("param");
                // Check for a re-assigned object binding, like { entityName: MyEntity }
                for (const binding of Object.values(paramBindings)) {
                    const bindingPath = binding.path;
                    if (bindingPath.isObjectPattern()) {
                        const properties = bindingPath.get("properties");
                        for (const property of properties) {
                            if (!property.isObjectProperty())
                                continue;
                            const keyName = getIdentiferName(property.get("key").node);
                            if (keyName === oldName && !property.node.shorthand) {
                                property.get("key").replaceInline(t.identifier(newName));
                                path.skip();
                                return;
                            }
                        }
                    }
                }
                // Simple case, the identifier is part of the param list, probably in a destructured object param
                if (path.scope.hasBinding(oldName) && !parentBinding) {
                    const binding = path.scope.getBinding(oldName);
                    // After we do a rename, we want to check if the original binding was an object pattern and
                    // restore the shorthand reference
                    path.scope.rename(oldName, newName);
                    const bindingPath = binding.path;
                    if (bindingPath.isObjectPattern()) {
                        for (const property of bindingPath.get("properties")) {
                            if (!property.isObjectProperty())
                                continue;
                            const keyName = getIdentiferName(property.get("key").node);
                            if (keyName === oldName) {
                                property.get("key").replaceInline(t.identifier(newName));
                                property.get("value").replaceInline(t.identifier(newName));
                                property.set("shorthand", true);
                            }
                        }
                    }
                    path.skip();
                    return;
                }
                const ids = path.getBindingIdentifierPaths();
                // The more complex case is when when the param is an identifier and we access the entity via a member expression
                // i.e. function doSomething(state) { return state.entityName.property; }
                for (const bindingName of Object.keys(ids)) {
                    const binding = path.scope.getBinding(bindingName);
                    if (!binding || binding.kind !== "param") {
                        continue;
                    }
                    for (const reference of binding.referencePaths) {
                        if (reference.parentPath?.isMemberExpression() ||
                            reference.parentPath?.isOptionalMemberExpression()) {
                            let property = reference.parentPath.get("property");
                            if (Array.isArray(property)) {
                                property = property[0];
                            }
                            // Because member expressions need to be unwound to get following properties, we need to go up one more level
                            // and check the object
                            if (property.isIdentifier() &&
                                property.node.name === parentBinding &&
                                reference.parentPath.parentPath?.isMemberExpression()) {
                                handleMemberExpression(reference.parentPath.parentPath);
                                path.skip();
                                return;
                            }
                            if (property.isIdentifier() && property.node.name === oldName) {
                                property.replaceInline(t.identifier(newName));
                                path.skip();
                            }
                        }
                    }
                }
            },
        }
        : {};
    let traversePath = nodePath;
    if (traversePath.isFunction()) {
        traversePath = traversePath.parentPath;
    }
    function handleMemberExpression(path) {
        // this captures a computed property like SomeObject[oldName] (look at the tests)
        if (path.node.computed) {
            const property = path.get("property");
            if (property.isIdentifier() && property.node.name === oldName) {
                property.replaceInline(t.identifier(newName));
                path.skip();
                return;
            }
        }
        const components = [];
        let current = path;
        // MemberExpressions need to be unwound backwards, because of the way they are
        // nested by babel
        while (current.isMemberExpression() ||
            current.isOptionalMemberExpression()) {
            const property = current.get("property");
            // this shouldnt happen, the babel type is weird
            if (Array.isArray(property)) {
                break;
            }
            if (property.isIdentifier()) {
                components.unshift({ name: property.node.name, path: property });
            }
            const object = current.get("object");
            // again, this shouldnt happen, the babel type is weird
            if (Array.isArray(object)) {
                break;
            }
            current = object;
        }
        if (current.isIdentifier()) {
            components.unshift({ name: current.node.name, path: current });
        }
        if (components.length === 0) {
            return;
        }
        // handle the case where we want to rename something on app scope, like App.stateVar
        if (parentBinding) {
            for (let i = 0; i < components.length - 1; i++) {
                if (components[i]?.name === parentBinding &&
                    components[i + 1]?.name === oldName) {
                    components[i + 1]?.path.replaceInline(t.identifier(newName));
                    path.skip();
                    return;
                }
            }
        }
        else {
            // otherwise its a simple replace
            for (const component of components) {
                if (component.name === oldName) {
                    component.path.replaceInline(t.identifier(newName));
                    path.skip();
                    return;
                }
            }
        }
    }
    function handleEventFlowChain(path) {
        // Check if this is an EventFlow identifier
        if (path.node.name !== "EventFlow") {
            return;
        }
        // Find the CallExpression that contains this EventFlow identifier
        let current = path;
        while (current && !current.isCallExpression()) {
            current = current.parentPath;
        }
        if (!current || !current.isCallExpression()) {
            return;
        }
        // This is an EventFlow call, now traverse through the entire chain
        // Start from the current call (innermost) and walk up to find all chained calls
        let currentCall = current;
        // Walk up to find all chained calls and process them
        while (currentCall.parentPath &&
            currentCall.parentPath.isMemberExpression()) {
            // Process the current call
            const args = currentCall.get("arguments");
            if (args.length > 0) {
                const firstArg = args[0];
                if (firstArg &&
                    firstArg.isIdentifier() &&
                    firstArg.node.name === oldName) {
                    firstArg.replaceInline(t.identifier(newName));
                }
            }
            const parentMemberExpression = currentCall.parentPath;
            if (parentMemberExpression.parentPath &&
                parentMemberExpression.parentPath.isCallExpression()) {
                // Current call is the object of a MemberExpression that is the callee of a CallExpression
                // Move up to the CallExpression (the chained call)
                currentCall = parentMemberExpression.parentPath;
            }
            else {
                // Not a chained call
                break;
            }
        }
        // Process the final (outermost) call
        const args = currentCall.get("arguments");
        if (args.length > 0) {
            const firstArg = args[0];
            if (firstArg &&
                firstArg.isIdentifier() &&
                firstArg.node.name === oldName) {
                firstArg.replaceInline(t.identifier(newName));
            }
        }
        path.skip();
    }
    // State to track if we're inside a template literal
    let isInsideTemplateLiteral = false;
    traversePath.traverse({
        ...functionVisitor,
        OptionalMemberExpression(path) {
            handleMemberExpression(path);
        },
        MemberExpression(path) {
            handleMemberExpression(path);
        },
        TemplateLiteral: {
            enter(_path) {
                isInsideTemplateLiteral = true;
            },
            exit(_path) {
                isInsideTemplateLiteral = false;
            },
        },
        Identifier(path) {
            if (path.node.name === "EventFlow") {
                handleEventFlowChain(path);
                return;
            }
            if (path.node.name === oldName) {
                if (path.parentPath?.isArrayExpression()) {
                    path.replaceInline(t.identifier(newName));
                    return;
                }
                // Check if this identifier is within a template literal context
                if (isInsideTemplateLiteral) {
                    path.replaceInline(t.identifier(newName));
                    return;
                }
            }
        },
        CallExpression(path) {
            const args = path.get("arguments");
            if (args.length === 0) {
                return;
            }
            for (const arg of args) {
                if (arg.isIdentifier() && getIdentiferName(arg.node) === oldName) {
                    arg.replaceInline(t.identifier(newName));
                }
            }
        },
        VariableDeclarator(path) {
            const init = path.get("init");
            if (init.isIdentifier() && getIdentiferName(init.node) === oldName) {
                init.replaceInline(t.identifier(newName));
            }
            const id = path.get("id");
            if (id.isIdentifier() && getIdentiferName(id.node) === oldName) {
                id.replaceInline(t.identifier(newName));
            }
        },
    });
}
//# sourceMappingURL=javascript.js.map