import path from "node:path";
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { isPropertyInfo, splitJSPath, splitJSPathAdvanced, } from "@superblocksteam/library-shared";
import { format } from "prettier";
import { normalizePath } from "vite";
import { generateJSXAttribute, generateJSXElement, writeNestedProperty, } from "./codegen.js";
import { ComponentsManager } from "./components-manager.js";
import { generate } from "./generate.js";
import { generatePredictableId } from "./ids.js";
import { getSbElementId, setSbElementId, supplementElementIds, } from "./parsing/ids.js";
import { getRequiredImportsForProperty, EXTERNAL_LIBRARY_OPTIONS, } from "./parsing/imports.js";
import { getDescriptiveName, getJSXAttributeName, getVariableReferences, } from "./parsing/jsx.js";
import { getTagName, addImport, extractImportsFromAst, } from "./parsing/util.js";
import { RenameManager } from "./rename-manager.js";
import { getErrorMeta, getLogger } from "./util/logger.js";
import { getScopeIdFromName } from "./util.js";
const ATTRIBUTE_TYPES_WITH_STATE_ACCESS = ["COMPUTED", "TEMPLATE", "EVENT"];
// File Explanation:
// This file is responsible for tracking the source code of the application.
// It parses the source code of the application and stores the ASTs of the files.
// It also tracks the location of JSX elements in the source code.
// This information is used by the source code editor to provide features like property editing.
export class SourceTracker {
    renameManager = new RenameManager();
    elementToLocation = {};
    elementToFilePath = {};
    newlyAddedCount = 0;
    fileToMeta = {};
    changedFiles = new Set();
    constructor(_tracer) { }
    /**
     * Add a file to the changed files set with normalized path (Windows compatibility)
     */
    addChangedFile(filePath) {
        this.changedFiles.add(normalizePath(filePath));
    }
    getCurrentFiles() {
        return this.fileToMeta;
    }
    /**
     * Retrieves and clears the changes made to files, returning them as a record of file
     * names and their corresponding code.
     * @returns A Record<string, string> object containing the file names as keys and their corresponding code as values.
     */
    getAndFlushChanges = async () => {
        const files = Array.from(this.changedFiles);
        this.changedFiles.clear();
        return await Promise.all(files.map(async (file) => {
            const meta = this.fileToMeta[file];
            if (!meta)
                throw new Error("No meta found for file " + file);
            try {
                const code = generate(meta.ast, {
                    jsescOption: {
                        minimal: true,
                    },
                }).code;
                const formattedCode = await format(code, {
                    parser: "typescript",
                });
                return { fileName: file, source: formattedCode, kind: "ts" };
            }
            catch (e) {
                getLogger().error(`Error generating code for file ${file}`, getErrorMeta(e));
                return { fileName: file, source: meta.code, kind: "ts" };
            }
        }));
    };
    getElementToFilePath(el) {
        if (el === undefined)
            return this.elementToFilePath;
        return this.elementToFilePath[el];
    }
    getElementToLocation(el) {
        if (el === undefined)
            return this.elementToLocation;
        return this.elementToLocation[el];
    }
    setElementToLocation(el, path) {
        this.elementToLocation[el] = path;
    }
    setElementToFilePath(el, filePath) {
        this.elementToFilePath[el] = filePath;
    }
    deleteElementToLocation(el) {
        delete this.elementToLocation[el];
    }
    deleteElementToFilePath(el) {
        delete this.elementToFilePath[el];
    }
    getScopeIdForPage(pageName) {
        return getScopeIdFromName(pageName);
    }
    parseJsxFromEntrypoint({ files, }) {
        const parsedAsts = {};
        for (const { fileName, code } of files) {
            const isTsx = fileName.endsWith(".tsx");
            const isTs = fileName.endsWith(".ts");
            if (isTsx || isTs) {
                try {
                    parsedAsts[fileName] = parse(code, {
                        sourceType: "module",
                        sourceFilename: fileName,
                        plugins: ["typescript", "jsx"],
                    });
                }
                catch (e) {
                    getLogger().error(`Error parsing entry point: ${path.relative(process.cwd(), fileName)}`, getErrorMeta(e));
                }
            }
        }
        return parsedAsts;
    }
    initializeSourceTracker = ({ files }) => {
        const fileContents = Object.entries(files).map(([fileName, code]) => ({
            fileName,
            code,
        }));
        const parsedAsts = this.parseJsxFromEntrypoint({
            files: fileContents,
        });
        // Clean up files that no longer exist
        const currentFileNames = new Set(Object.keys(files));
        const trackedFileNames = Object.keys(this.fileToMeta);
        for (const trackedFile of trackedFileNames) {
            if (!currentFileNames.has(trackedFile)) {
                this.removeFile(trackedFile);
            }
        }
        Object.entries(parsedAsts).forEach(([file, ast]) => {
            // we do not need to reparse code we already have but hasnt changed
            if (file in this.fileToMeta &&
                files[file] === this.fileToMeta[file]?.code) {
                return;
            }
            this.fileToMeta[file] = {
                code: fileContents.find((f) => f.fileName === file).code,
                ast,
                imports: extractImportsFromAst(ast),
            };
        });
        // Parse JSX elements and track their locations
        Object.entries(parsedAsts).forEach(([file, ast]) => {
            const { idMap } = supplementElementIds({
                fileName: file,
                ast,
                shouldModifyAst: false,
            });
            idMap.forEach((path, id) => {
                this.elementToLocation[id] = path;
                this.elementToFilePath[id] = file;
            });
        });
    };
    handleNonVisualChangeByDeletingIds = async (fileName, fileContents) => {
        const extension = path.extname(fileName);
        if (extension !== ".ts" && extension !== ".tsx")
            return;
        try {
            const astMap = this.parseJsxFromEntrypoint({
                files: [{ fileName, code: fileContents }],
            });
            const ast = astMap[fileName];
            if (!ast) {
                throw new Error("Failed to parse file " + fileName);
            }
            this.fileToMeta[fileName] = {
                ast: ast,
                code: fileContents,
                imports: extractImportsFromAst(ast),
            };
            // delete all existing elements from the ast cache
            Object.entries(this.elementToLocation)
                .filter(([id]) => {
                const filePath = this.getElementToFilePath(id);
                if (!filePath)
                    return false;
                return filePath.startsWith(fileName);
            })
                .forEach(([key]) => {
                delete this.elementToLocation[key];
                delete this.elementToFilePath[key];
            });
            // Parse JSX elements and track their locations
            const { idMap } = supplementElementIds({
                fileName,
                ast,
                shouldModifyAst: false,
            });
            idMap.forEach((path, id) => {
                this.elementToLocation[id] = path;
                this.elementToFilePath[id] = fileName;
            });
            // Mark the file as changed
            this.addChangedFile(fileName);
        }
        catch (e) {
            getLogger().error("Non-visual change parsing error", getErrorMeta(e));
            return;
        }
    };
    removeFile = (fileName) => {
        delete this.fileToMeta[fileName];
        // Clean up element mappings
        Object.entries(this.elementToFilePath).forEach(([id, path]) => {
            if (path === fileName) {
                delete this.elementToLocation[id];
                delete this.elementToFilePath[id];
            }
        });
    };
    updateFileContents(filePath, code) {
        const existingFile = this.fileToMeta[filePath];
        if (!existingFile ||
            (existingFile?.ast != null && existingFile?.code === code)) {
            return;
        }
        existingFile.code = code;
    }
    addApi = async ({ apiName: _apiName }) => {
        // Return the app-level scope ID
        return getScopeIdFromName("App");
    };
    deleteApi = ({ apiName: _apiName, }) => {
        // Return the app-level scope ID
        return getScopeIdFromName("App");
    };
    addElement = ({ parentElement, tagName, properties, children, id, scopeName: _scopeName, }) => {
        const logger = getLogger();
        const parentOpeningElementPath = this.getElementToLocation(parentElement.source.id);
        const parentFilepath = this.getElementToFilePath(parentElement.source.id);
        if (!parentOpeningElementPath || !parentFilepath) {
            logger.error(`Element not found in source tracker ${parentElement.source.id} ${parentOpeningElementPath?.toString()}`);
            throw new Error("Element not found in source tracker " + parentElement.source.id);
        }
        if (!parentOpeningElementPath.isJSXOpeningElement()) {
            logger.error(`Parent element is not a JSX element ${parentOpeningElementPath.type} ${parentOpeningElementPath.toString()}`);
            throw new Error("Parent element is not a JSX element");
        }
        const parentJSXElementPath = parentOpeningElementPath.parentPath;
        if (!parentJSXElementPath?.isJSXElement()) {
            logger.error(`Parent element is not a JSX element ${String(parentJSXElementPath)}`);
            throw new Error("Parent element is not a JSX element");
        }
        try {
            const newElement = generateJSXElement({
                id,
                tagName,
                properties,
                children,
            });
            this.transformSelfClosingTagToRegularTag(parentJSXElementPath);
            const { index = -1 } = parentElement.source;
            const elementChildren = parentJSXElementPath
                .get("children")
                .filter((child) => child.isJSXElement());
            let results = null;
            if (index === 0) {
                results = parentJSXElementPath.unshiftContainer("children", newElement);
            }
            else if (index < 0 || index >= elementChildren.length) {
                results = parentJSXElementPath.pushContainer("children", newElement);
            }
            else {
                const child = elementChildren[index];
                if (!child)
                    throw new Error("Child element not found");
                results = child.insertBefore(newElement);
            }
            if (!Array.isArray(results)) {
                throw new Error("Failed to add element to parent");
            }
            // You can traverse subpaths like this: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#paths-in-visitors
            let sourceId;
            results[0]?.traverse({
                JSXOpeningElement: (path) => {
                    if (path.node.name.type !== "JSXIdentifier")
                        return;
                    const tagName = getTagName(path.node.name);
                    if (!tagName)
                        return;
                    this.newlyAddedCount++;
                    const id = getSbElementId(path.node) ??
                        `SB-${generatePredictableId(parentFilepath + this.newlyAddedCount)}`;
                    setSbElementId(path.node, id);
                    this.elementToLocation[id] = path;
                    this.elementToFilePath[id] = parentFilepath;
                    sourceId = id;
                },
            });
            const manager = ComponentsManager.getInstance();
            if (manager.isCustomComponent(tagName)) {
                const importPath = manager.getComponentImportPath(tagName, process.cwd());
                if (importPath) {
                    addImport({
                        path: parentOpeningElementPath,
                        importName: tagName,
                        importPath,
                        isDefaultImport: true,
                        onAdd: () => {
                            this.fileToMeta[parentFilepath].imports[tagName] = importPath;
                        },
                    });
                }
            }
            else {
                addImport({
                    path: parentOpeningElementPath,
                    importName: tagName,
                    importPath: "@superblocksteam/library",
                    onAdd: () => {
                        this.fileToMeta[parentFilepath].imports[tagName] =
                            "@superblocksteam/library";
                    },
                });
            }
            // We need to add the imports for the properties that are used in the new element
            const requiredImports = this.getAddElementImports({
                properties,
                children,
            });
            requiredImports.forEach(({ importName, importPath, isDefaultImport }) => {
                addImport({
                    path: parentOpeningElementPath,
                    importName,
                    importPath,
                    isDefaultImport,
                    onAdd: () => {
                        this.fileToMeta[parentFilepath].imports[importName] = importPath;
                    },
                });
            });
            this.addChangedFile(parentFilepath);
            return sourceId;
        }
        catch (e) {
            getLogger().error("Error updating source tracker during add", getErrorMeta(e));
            throw new Error("Error updating source tracker during add" + e.message);
        }
    };
    deleteElement = ({ source, scopeName: _scopeName, }) => {
        const logger = getLogger();
        let openingTag = this.getElementToLocation(source.id);
        if (!openingTag) {
            logger.error(`Element not found in source tracker ${source.id}`);
            return;
        }
        if (openingTag.isJSXOpeningElement()) {
            const parent = openingTag.parentPath;
            if (parent?.isJSXElement()) {
                openingTag = parent;
            }
            else {
                logger.error(`JSXOpeningElement does not have a JSXElement parent ${source.id}`);
                return;
            }
        }
        else if (!openingTag.isJSXElement()) {
            logger.error(`Cannot delete a non-JSX element ${source.id}`);
            return;
        }
        const _componentName = getDescriptiveName(openingTag.node);
        const children = [];
        openingTag.traverse({
            JSXElement: (path) => {
                const sbElementId = getSbElementId(path.get("openingElement").node);
                const name = getDescriptiveName(path.node);
                if (sbElementId) {
                    children.push({ id: sbElementId, name });
                }
            },
        });
        const fileName = this.getElementToFilePath(source.id);
        this.deleteSectionIfOnlyChild(openingTag);
        const wasAssignedToVariable = this.tryDeleteJSXReferenceIfAssigned(openingTag);
        if (!wasAssignedToVariable) {
            openingTag.remove();
        }
        delete this.elementToLocation[source.id];
        delete this.elementToFilePath[source.id];
        // Clean up child element tracking
        children.forEach(({ id }) => {
            delete this.elementToLocation[id];
            delete this.elementToFilePath[id];
        });
        this.addChangedFile(fileName);
    };
    tryDeleteJSXReferenceIfAssigned = (jsxElement) => {
        try {
            // Cases like: const card = <JSX />
            const parentPath = jsxElement.parentPath;
            if (parentPath?.isVariableDeclarator() &&
                parentPath.get("init") === jsxElement) {
                const id = parentPath.get("id");
                if (id.isIdentifier()) {
                    const binding = jsxElement.scope.getBinding(id.node.name);
                    if (binding) {
                        this.removeReferencesUsingBinding(binding);
                        return true;
                    }
                }
            }
            const scopePath = jsxElement.scope.path;
            const isFunctionParent = scopePath.isArrowFunctionExpression() ||
                scopePath.isFunctionExpression();
            if (!isFunctionParent) {
                return false;
            }
            // Look for function wrapper assignments: const card = useMemo(() => <JSX />, [])
            const allBindings = jsxElement.scope.getAllBindings();
            const matchingBinding = Object.values(allBindings).find((binding) => {
                if (!binding.path.isVariableDeclarator()) {
                    return false;
                }
                const init = binding.path.get("init");
                if (init.node &&
                    this.isJSXElementInVariableInit(jsxElement, init)) {
                    return true;
                }
                return false;
            });
            if (matchingBinding) {
                this.removeReferencesUsingBinding(matchingBinding);
                return true;
            }
        }
        catch (error) {
            console.warn("Binding-based JSX reference deletion failed:", error);
        }
        return false;
    };
    removeReferencesUsingBinding = (binding) => {
        binding.referencePaths.forEach((referencePath) => {
            const parent = referencePath.parentPath;
            if (parent?.isJSXExpressionContainer()) {
                parent.remove();
            }
            else if (parent?.isCallExpression() &&
                parent.parentPath?.isJSXExpressionContainer()) {
                parent.parentPath.remove();
            }
        });
    };
    // Check if a JSX element is contained within a variable's initializer (e.g., in useMemo, useCallback, etc.)
    isJSXElementInVariableInit = (jsxElement, init) => {
        let found = false;
        // Only check for function calls (like useMemo, useCallback) or short arrow functions
        if (init.isCallExpression()) {
            init.traverse({
                JSXElement(path) {
                    if (path === jsxElement) {
                        found = true;
                        path.stop();
                    }
                },
            });
        }
        else if (init.isArrowFunctionExpression() ||
            init.isFunctionExpression()) {
            const body = init.get("body");
            if (!Array.isArray(body) && body.isJSXElement() && body === jsxElement) {
                found = true;
            }
            else if (!Array.isArray(body) && body.isBlockStatement()) {
                const statements = body.get("body");
                if (Array.isArray(statements) &&
                    statements.length === 1 &&
                    statements[0]?.isReturnStatement()) {
                    const returnArg = statements[0].get("argument");
                    if (returnArg === jsxElement) {
                        found = true;
                    }
                }
            }
        }
        return found;
    };
    // If we're deleting the only Column in an Section, remove the Section
    deleteSectionIfOnlyChild = (openingTag) => {
        const parentElement = openingTag.parentPath;
        if (!parentElement)
            return false;
        const isColumn = getTagName(openingTag.node.openingElement.name) ===
            "Column";
        const isOnlyChild = parentElement.isJSXElement() &&
            parentElement.get("children").filter((child) => child.isJSXElement())
                .length === 1;
        const isParentSection = parentElement.isJSXElement() &&
            getTagName(parentElement.node.openingElement.name) ===
                "Section";
        if (isColumn && isOnlyChild && isParentSection) {
            const sectionId = getSbElementId(parentElement.get("openingElement").node);
            if (sectionId) {
                delete this.elementToLocation[sectionId];
                delete this.elementToFilePath[sectionId];
                parentElement.remove();
            }
        }
    };
    /**
     * Set's a deeply nested property on a JSX attribute. For example, if you have an attribute that is
     * an object like:
     *   ```
     *   columns={{
     *     name: {
     *       accessor: "name",
     *     },
     *   }}
     *   ```
     * And you want to update the `accessor` property, you can use this function to do so, passing a property path like
     * `columns.name.accessor`, rather than the entire columns object.
     */
    setNestedAttribute = ({ openingTag, property, info, }) => {
        const [parent, ...paths] = splitJSPathAdvanced(property, []);
        let parentAttr = openingTag
            .get("attributes")
            .find((attr) => attr.isJSXAttribute() && attr.node.name.name === parent);
        // If value is undefined, we want to delete the nested property
        if (info.value === undefined) {
            if (!parentAttr) {
                // Property doesn't exist anyway, nothing to delete
                return;
            }
            const expressionContainer = parentAttr?.get("value");
            if (!expressionContainer ||
                !expressionContainer.isJSXExpressionContainer()) {
                // Can't delete from non-object, just return
                return;
            }
            const currentNode = expressionContainer.get("expression");
            writeNestedProperty({
                node: currentNode,
                paths,
                info,
                isDelete: true,
            });
            return;
        }
        if (!parentAttr && parent) {
            const newAttr = openingTag.pushContainer("attributes", [
                t.jsxAttribute(t.jsxIdentifier(parent), t.jSXExpressionContainer(t.objectExpression([]))),
            ]);
            parentAttr = newAttr[0];
        }
        const expressionContainer = parentAttr?.get("value");
        if (!expressionContainer ||
            !expressionContainer.isJSXExpressionContainer()) {
            throw new Error("Parent attribute value is not a JSXExpressionContainer");
        }
        const currentNode = expressionContainer.get("expression");
        writeNestedProperty({
            node: currentNode,
            paths,
            info,
        });
        return currentNode.node;
    };
    setAttribute = (openingTag, property, info) => {
        const paths = splitJSPath(property);
        if (Array.isArray(paths) && paths.length > 1) {
            // Sometimes we want to set a nested attribute, like `columns.name.accessor`
            return this.setNestedAttribute({ openingTag, property, info });
        }
        let attr = openingTag
            .get("attributes")
            .find((attr) => attr.isJSXAttribute() && attr.node.name.name === property);
        if (!attr) {
            attr = openingTag.pushContainer("attributes", [
                t.jsxAttribute(t.jsxIdentifier(property), t.stringLiteral("")),
            ])[0];
        }
        const value = info.value;
        // We currently delete attributes that are undefined or set to an empty string
        if (value === undefined || (typeof value === "string" && value === "")) {
            attr.remove();
            return;
        }
        const attrName = getJSXAttributeName(attr.node);
        if (attrName === null) {
            throw new Error("Attribute name not found");
        }
        const attrValue = attr.get("value");
        const jsxAttribute = generateJSXAttribute(property, info);
        if (jsxAttribute.value) {
            attrValue.replaceWith(jsxAttribute.value);
        }
        return jsxAttribute;
    };
    deleteAttribute = (openingTag, property) => {
        const attr = openingTag
            .get("attributes")
            .find((attr) => attr.isJSXAttribute() && attr.node.name.name === property);
        attr?.remove();
    };
    transformSelfClosingTagToRegularTag = (element) => {
        const openingElement = element.get("openingElement");
        if (openingElement.node.selfClosing) {
            openingElement.node.selfClosing = false;
            element.node.closingElement = t.jsxClosingElement(openingElement.node.name);
        }
    };
    setJSXChildren = (jsxElement, info) => {
        const logger = getLogger();
        try {
            // Ensure the element is not self-closing
            this.transformSelfClosingTagToRegularTag(jsxElement);
            // Clear existing children
            jsxElement.node.children = [];
            // Handle different types of children content
            if (info.type === "STATIC" && typeof info.value === "string") {
                // For string children, create a text node
                if (info.value.trim() !== "") {
                    jsxElement.node.children = [t.jsxText(info.value)];
                }
            }
            else {
                console.warn("setting non-static string children is not supported");
            }
        }
        catch (e) {
            logger.error("Error setting JSX children", getErrorMeta(e));
        }
    };
    setProperty = ({ source, property, info, }) => {
        this.setProperties({
            source,
            changes: { [property]: info },
        });
    };
    setProperties = ({ source, changes, }) => {
        const logger = getLogger();
        if (Object.keys(changes).length === 0)
            return;
        try {
            let openingTag = this.getElementToLocation(source.id);
            if (openingTag?.isJSXElement()) {
                openingTag = openingTag.get("openingElement");
            }
            const fileName = this.getElementToFilePath(source.id);
            if (!fileName) {
                logger.error(`Filepath not found for element ${source.id} ${openingTag}`);
                throw new Error(`Filepath not found for element ${source.id}`);
            }
            const currentFile = this.fileToMeta[fileName];
            if (!currentFile || !openingTag) {
                throw new Error(`Element ${source.id} not found in source tracker`);
            }
            if (!openingTag.isJSXOpeningElement()) {
                throw new Error(`Element ${source.id} is not a JSXOpeningElement`);
            }
            // Get the JSX element to handle children
            let jsxElement = this.getElementToLocation(source.id);
            if (jsxElement?.isJSXOpeningElement()) {
                jsxElement = jsxElement.parentPath;
            }
            // Iterate over the changes and set each attribute using the helper
            for (const [key, info] of Object.entries(changes)) {
                // Special handling for children property
                if (key === "children") {
                    const childrenAttribute = openingTag
                        .get("attributes")
                        .find((attr) => attr.isJSXAttribute() && attr.node.name.name === "children");
                    const jsxChildrenExists = jsxElement.isJSXElement() && jsxElement.node.children.length > 0;
                    // We always set the jsx children UNLESS children exists as an attribute and there are no current jsx children
                    if (jsxChildrenExists || !childrenAttribute) {
                        this.setJSXChildren(jsxElement, info);
                        if (childrenAttribute) {
                            childrenAttribute.remove();
                        }
                        continue;
                    }
                }
                const jsxAttribute = this.setAttribute(openingTag, key, info);
                const imports = getRequiredImportsForProperty(info);
                if (imports.length > 0) {
                    for (const { importName, importPath } of imports) {
                        if (!this.fileToMeta[fileName].imports[importName]) {
                            addImport({
                                importPath,
                                importName,
                                path: openingTag,
                                onAdd: () => {
                                    this.fileToMeta[fileName].imports[importName] = importPath;
                                },
                            });
                        }
                    }
                }
                if (ATTRIBUTE_TYPES_WITH_STATE_ACCESS.includes(info.type) &&
                    jsxAttribute) {
                    // Add external library imports (but not scope imports)
                    for (const externalLibrary of EXTERNAL_LIBRARY_OPTIONS) {
                        const referencedGlobalFunctions = getVariableReferences(jsxAttribute, externalLibrary.imports).map((funcName) => {
                            return {
                                importPath: externalLibrary.library,
                                importName: funcName,
                            };
                        });
                        referencedGlobalFunctions.forEach(({ importPath, importName }) => {
                            addImport({
                                importPath,
                                importName,
                                path: openingTag,
                                onAdd: () => {
                                    this.fileToMeta[fileName].imports[importName] = importPath;
                                },
                            });
                        });
                    }
                }
            }
            this.addChangedFile(fileName);
        }
        catch (e) {
            logger.error("Error setting property", getErrorMeta(e));
        }
    };
    deleteProperties = async ({ source, properties, }) => {
        const logger = getLogger();
        try {
            let openingTag = this.elementToLocation[source.id];
            if (openingTag?.isJSXElement()) {
                openingTag = openingTag.get("openingElement");
            }
            const fileName = this.getElementToFilePath(source.id);
            if (!fileName) {
                logger.error(`Filepath not found for element ${source.id} ${openingTag}`);
                throw new Error(`Filepath not found for element ${source.id}`);
            }
            const currentFile = this.fileToMeta[fileName];
            if (!currentFile || !openingTag) {
                throw new Error(`Element ${source.id} not found in source tracker`);
            }
            if (!openingTag.isJSXOpeningElement()) {
                throw new Error(`Element ${source.id} is not a JSXOpeningElement`);
            }
            for (const property of properties) {
                this.deleteAttribute(openingTag, property);
            }
            this.addChangedFile(fileName);
        }
        catch (e) {
            logger.error("Error deleting properties", getErrorMeta(e));
        }
    };
    renameComponent = async ({ widgetSourceId, newName, oldName: _oldName, renameInAst = true, }) => {
        const filePath = this.getElementToFilePath(widgetSourceId);
        const currentFile = this.fileToMeta[filePath];
        if (!currentFile || !currentFile.ast) {
            throw new Error(`File not found for element ${filePath}`);
        }
        let widgetNode = this.getElementToLocation(widgetSourceId);
        if (!widgetNode) {
            throw new Error(`Widget ${widgetSourceId} not found`);
        }
        if (widgetNode?.isJSXElement()) {
            widgetNode = widgetNode.get("openingElement");
        }
        await this.renameManager.renameComponent({
            widgetNode: widgetNode,
            newName,
            ast: currentFile.ast,
            renameInAst,
        });
        this.addChangedFile(filePath);
    };
    renameIdentifier = ({ oldName, newName, filePath, parentBinding, }) => {
        const currentFile = this.fileToMeta[filePath];
        if (!currentFile || !currentFile.ast) {
            throw new Error(`File not found for element ${filePath}`);
        }
        const imports = extractImportsFromAst(currentFile.ast);
        if (newName in imports) {
            throw new Error(`Cannot rename identifier ${oldName} to ${newName} because it has a naming collision with an existing import in file ${filePath}`);
        }
        this.renameManager.renameIdentifierInAST({
            oldName,
            newName,
            ast: currentFile.ast,
            parentBinding,
        });
        this.addChangedFile(filePath);
    };
    getAddElementImports = ({ properties, children, }) => {
        const componentsManager = ComponentsManager.getInstance();
        const getPropertyImports = (properties) => {
            return Object.values(properties)
                .flatMap((property) => {
                if (property.type === "DIMENSION") {
                    return {
                        importPath: "@superblocksteam/library",
                        importName: "Dim",
                    };
                }
                if (property.type === "BINDING") {
                    return { importPath: "@superblocksteam/library", importName: "SB" };
                }
                if (property.type === "STATIC" &&
                    typeof property.value === "object" &&
                    property.value != null) {
                    // Object.entries works on arrays as well, so we dont need to special case them for just finding property values
                    const staticProperties = Object.fromEntries(Object.entries(property.value).filter(([_, value]) => isPropertyInfo(value)));
                    return getPropertyImports(staticProperties);
                }
                return null;
            })
                .filter(Boolean);
        };
        const getComponentImports = (children) => {
            return children.flatMap((child) => {
                let defaultPath = "@superblocksteam/library";
                const isCustomComponent = componentsManager.isCustomComponent(child.tagName);
                if (isCustomComponent) {
                    const componentPath = componentsManager.getComponentImportPath(child.tagName, process.cwd());
                    if (componentPath) {
                        defaultPath = componentPath;
                    }
                }
                return [
                    {
                        importPath: defaultPath,
                        importName: child.tagName,
                        isDefaultImport: isCustomComponent,
                    },
                    ...getPropertyImports(child.properties),
                    ...getComponentImports(child.children),
                ];
            });
        };
        const requiredPropertyImports = getPropertyImports(properties);
        const requiredComponentImports = getComponentImports(children);
        const seen = new Set();
        return [...requiredPropertyImports, ...requiredComponentImports].filter((i) => {
            if (seen.has(i.importName)) {
                return false;
            }
            seen.add(i.importName);
            return true;
        });
    };
}
//# sourceMappingURL=source-tracker.js.map