import path from "node:path";
import { parse } from "@babel/parser";
import { format } from "prettier";
import { normalizePath } from "vite";
import { generate } from "./generate.js";
import { supplementElementIds } from "./parsing/ids.js";
import { extractImportsFromAst } from "./parsing/util.js";
import { getScopeIdFromName } from "./util.js";
import { getErrorMeta, getLogger } from "./util/logger.js";
// 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 for AI editing.
export class SourceTracker {
    elementToLocation = {};
    elementToFilePath = {};
    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];
    }
    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;
    }
    /**
     * Clears cached element mappings for a specific file so re-indexing
     * cannot retain stale element IDs from previous AST snapshots.
     */
    clearElementMappingsForFile(fileName) {
        Object.entries(this.elementToFilePath).forEach(([id, mappedFilePath]) => {
            if (mappedFilePath === fileName) {
                delete this.elementToLocation[id];
                delete this.elementToFilePath[id];
            }
        });
    }
    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]) => {
            this.fileToMeta[file] = {
                code: fileContents.find((f) => f.fileName === file).code,
                ast,
                imports: extractImportsFromAst(ast),
            };
        });
        // Remove prior IDs for files we are about to re-index.
        Object.keys(parsedAsts).forEach((file) => {
            this.clearElementMappingsForFile(file);
        });
        // 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;
            });
        });
    };
    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");
    };
}
//# sourceMappingURL=source-tracker.js.map