"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LSAndTSDocResolver = void 0;
const path_1 = require("path");
const typescript_1 = __importDefault(require("typescript"));
const utils_1 = require("../../utils");
const service_1 = require("./service");
const serviceCache_1 = require("./serviceCache");
const SnapshotManager_1 = require("./SnapshotManager");
const utils_2 = require("./utils");
const fileCollection_1 = require("../../lib/documents/fileCollection");
class LSAndTSDocResolver {
    constructor(docManager, workspaceUris, configManager, options) {
        this.docManager = docManager;
        this.workspaceUris = workspaceUris;
        this.configManager = configManager;
        this.options = options;
        /**
         * Create a svelte document -> should only be invoked with svelte files.
         */
        this.createDocument = (fileName, content) => {
            const uri = (0, utils_1.pathToUrl)(fileName);
            const document = this.docManager.openDocument({
                text: content,
                uri
            }, 
            /* openedByClient */ false);
            this.docManager.lockDocument(uri);
            return document;
        };
        this.extendedConfigCache = new Map();
        docManager.on('documentChange', (0, utils_1.debounceSameArg)(this.updateSnapshot.bind(this), (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri, 1000));
        // New files would cause typescript to rebuild its type-checker.
        // Open it immediately to reduce rebuilds in the startup
        // where multiple files and their dependencies
        // being loaded in a short period of times
        docManager.on('documentOpen', (document) => {
            if (document.openedByClient) {
                this.getOrCreateSnapshot(document);
            }
            else {
                this.updateSnapshot(document);
            }
            docManager.lockDocument(document.uri);
        });
        this.getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)((options?.tsSystem ?? typescript_1.default.sys).useCaseSensitiveFileNames);
        this.tsSystem = this.wrapWithPackageJsonMonitoring(this.options?.tsSystem ?? typescript_1.default.sys);
        this.globalSnapshotsManager = new SnapshotManager_1.GlobalSnapshotsManager(this.tsSystem);
        // Notify when new snapshots are created so external watchers (svelte-check)
        // can react dynamically (for example: add parent directories to file watchers).
        if (this.options?.onFileSnapshotCreated) {
            this.globalSnapshotsManager.onChange((fileName, newDocument) => {
                if (newDocument) {
                    try {
                        this.options?.onFileSnapshotCreated?.(fileName);
                    }
                    catch {
                        // best-effort; ignore errors in callback
                    }
                }
            });
        }
        this.userPreferencesAccessor = { preferences: this.getTsUserPreferences() };
        const projectService = (0, serviceCache_1.createProjectService)(this.tsSystem, this.userPreferencesAccessor);
        configManager.onChange(() => {
            const newPreferences = this.getTsUserPreferences();
            const autoImportConfigChanged = newPreferences.includePackageJsonAutoImports !==
                this.userPreferencesAccessor.preferences.includePackageJsonAutoImports;
            this.userPreferencesAccessor.preferences = newPreferences;
            if (autoImportConfigChanged) {
                (0, service_1.forAllServices)((service) => {
                    service.onAutoImportProviderSettingsChanged();
                });
            }
        });
        this.packageJsonWatchers = new fileCollection_1.FileMap(this.tsSystem.useCaseSensitiveFileNames);
        this.watchedDirectories = new fileCollection_1.FileSet(this.tsSystem.useCaseSensitiveFileNames);
        // workspaceUris are already watched during initialization
        for (const root of this.workspaceUris) {
            const rootPath = (0, utils_1.urlToPath)(root);
            if (rootPath) {
                this.watchedDirectories.add(rootPath);
            }
        }
        this.lsDocumentContext = {
            isSvelteCheck: !!this.options?.isSvelteCheck,
            ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
            createDocument: this.createDocument,
            transformOnTemplateError: !this.options?.isSvelteCheck,
            globalSnapshotsManager: this.globalSnapshotsManager,
            notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit,
            extendedConfigCache: this.extendedConfigCache,
            onProjectReloaded: this.options?.onProjectReloaded,
            watchTsConfig: !!this.options?.watch,
            tsSystem: this.tsSystem,
            projectService,
            watchDirectory: this.options?.watchDirectory
                ? this.watchDirectory.bind(this)
                : undefined,
            nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern,
            reportConfigError: this.options?.reportConfigError
        };
    }
    async getLSAndTSDoc(document) {
        const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document);
        return {
            tsDoc,
            lang: lsContainer.getService(),
            userPreferences,
            lsContainer
        };
    }
    /**
     * Retrieves the LS for operations that don't need cross-files information.
     * can save some time by not synchronizing languageService program
     */
    async getLsForSyntheticOperations(document) {
        const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document);
        return { tsDoc, userPreferences, lang: lsContainer.getService(/* skipSynchronize */ true) };
    }
    async getLSAndTSDocWorker(document) {
        const lsContainer = await this.getTSService(document.getFilePath() || '');
        const tsDoc = await this.getOrCreateSnapshot(document);
        const userPreferences = this.getUserPreferences(tsDoc);
        return { tsDoc, lsContainer, userPreferences };
    }
    async getOrCreateSnapshot(pathOrDoc) {
        const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || '';
        const tsService = await this.getTSService(filePath);
        return tsService.updateSnapshot(pathOrDoc);
    }
    async updateSnapshot(document) {
        const filePath = document.getFilePath();
        if (!filePath) {
            return;
        }
        // ensure no new service is created
        await this.updateExistingFile(filePath, (service) => service.updateSnapshot(document));
    }
    /**
     * Updates snapshot path in all existing ts services and retrieves snapshot
     */
    async updateSnapshotPath(oldPath, newPath) {
        const document = this.docManager.get((0, utils_1.pathToUrl)(oldPath));
        const isOpenedInClient = document?.openedByClient;
        for (const snapshot of this.globalSnapshotsManager.getByPrefix(oldPath)) {
            await this.deleteSnapshot(snapshot.filePath);
        }
        if (isOpenedInClient) {
            this.docManager.openClientDocument({
                uri: (0, utils_1.pathToUrl)(newPath),
                text: document.getText()
            });
        }
        else {
            // This may not be a file but a directory, still try
            await this.getOrCreateSnapshot(newPath);
        }
    }
    /**
     * Deletes snapshot in all existing ts services
     */
    async deleteSnapshot(filePath) {
        await (0, service_1.forAllServices)((service) => service.deleteSnapshot(filePath));
        const uri = (0, utils_1.pathToUrl)(filePath);
        if (this.docManager.get(uri)) {
            // Guard this call, due to race conditions it may already have been closed;
            // also this may not be a Svelte file
            this.docManager.closeDocument(uri);
        }
        this.docManager.releaseDocument(uri);
    }
    async invalidateModuleCache(filePaths) {
        await (0, service_1.forAllServices)((service) => service.invalidateModuleCache(filePaths));
    }
    /**
     * Updates project files in all existing ts services
     */
    async updateProjectFiles(watcherNewFiles) {
        await (0, service_1.forAllServices)((service) => service.scheduleProjectFileUpdate(watcherNewFiles));
    }
    /**
     * Updates file in all ts services where it exists
     */
    async updateExistingTsOrJsFile(path, changes) {
        await this.updateExistingFile(path, (service) => service.updateTsOrJsFile(path, changes));
    }
    async updateExistingSvelteFile(path) {
        const newDocument = this.createDocument(path, this.tsSystem.readFile(path) ?? '');
        await this.updateExistingFile(path, (service) => {
            service.updateSnapshot(newDocument);
        });
    }
    async updateExistingFile(path, cb) {
        path = (0, utils_1.normalizePath)(path);
        // Only update once because all snapshots are shared between
        // services. Since we don't have a current version of TS/JS
        // files, the operation wouldn't be idempotent.
        let didUpdate = false;
        await (0, service_1.forAllServices)((service) => {
            if (service.hasFile(path) && !didUpdate) {
                didUpdate = true;
                cb(service);
            }
        });
    }
    async getTSService(filePath) {
        if (this.options?.tsconfigPath) {
            return this.getTSServiceByConfigPath(this.options.tsconfigPath, (0, path_1.dirname)(this.options.tsconfigPath));
        }
        if (!filePath) {
            throw new Error('Cannot call getTSService without filePath and without tsconfigPath');
        }
        return (0, service_1.getService)(filePath, this.workspaceUris, this.lsDocumentContext);
    }
    async getTSServiceByConfigPath(tsconfigPath, workspacePath) {
        return (0, service_1.getServiceForTsconfig)(tsconfigPath, workspacePath, this.lsDocumentContext);
    }
    getUserPreferences(tsDoc) {
        const configLang = tsDoc.scriptKind === typescript_1.default.ScriptKind.TS || tsDoc.scriptKind === typescript_1.default.ScriptKind.TSX
            ? 'typescript'
            : 'javascript';
        const nearestWorkspaceUri = this.workspaceUris.find((workspaceUri) => (0, utils_2.isSubPath)(workspaceUri, tsDoc.filePath, this.getCanonicalFileName));
        return this.configManager.getTsUserPreferences(configLang, nearestWorkspaceUri ? (0, utils_1.urlToPath)(nearestWorkspaceUri) : null);
    }
    getTsUserPreferences() {
        return this.configManager.getTsUserPreferences('typescript', null);
    }
    wrapWithPackageJsonMonitoring(sys) {
        if (!sys.watchFile || !this.options?.watch) {
            return sys;
        }
        const watchFile = sys.watchFile;
        return {
            ...sys,
            readFile: (path, encoding) => {
                if (path.endsWith('package.json') && !this.packageJsonWatchers.has(path)) {
                    this.packageJsonWatchers.set(path, watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000));
                }
                return sys.readFile(path, encoding);
            }
        };
    }
    onPackageJsonWatchChange(path, onWatchChange) {
        const dir = (0, path_1.dirname)(path);
        const projectService = this.lsDocumentContext.projectService;
        const packageJsonCache = projectService?.packageJsonCache;
        const normalizedPath = projectService?.toPath(path);
        if (onWatchChange === typescript_1.default.FileWatcherEventKind.Deleted) {
            this.packageJsonWatchers.get(path)?.close();
            this.packageJsonWatchers.delete(path);
            packageJsonCache?.delete(normalizedPath);
        }
        else {
            packageJsonCache?.addOrUpdate(normalizedPath);
        }
        (0, service_1.forAllServices)((service) => {
            service.onPackageJsonChange(path);
        });
        if (!path.includes('node_modules')) {
            return;
        }
        setTimeout(() => {
            this.updateSnapshotsInDirectory(dir);
            const realPath = this.tsSystem.realpath &&
                this.getCanonicalFileName((0, utils_1.normalizePath)(this.tsSystem.realpath?.(dir)));
            // pnpm
            if (realPath && realPath !== dir) {
                this.updateSnapshotsInDirectory(realPath);
                const realPkgPath = (0, path_1.join)(realPath, 'package.json');
                (0, service_1.forAllServices)((service) => {
                    service.onPackageJsonChange(realPkgPath);
                });
            }
        }, 500);
    }
    updateSnapshotsInDirectory(dir) {
        this.globalSnapshotsManager.getByPrefix(dir).forEach((snapshot) => {
            this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath);
        });
    }
    watchDirectory(patterns) {
        if (!this.options?.watchDirectory || patterns.length === 0) {
            return;
        }
        for (const pattern of patterns) {
            const uri = typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri;
            for (const watched of this.watchedDirectories) {
                if ((0, utils_2.isSubPath)(watched, uri, this.getCanonicalFileName)) {
                    return;
                }
            }
        }
        this.options.watchDirectory(patterns);
    }
}
exports.LSAndTSDocResolver = LSAndTSDocResolver;
//# sourceMappingURL=LSAndTSDocResolver.js.map