"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ignoredBuildDirectories = exports.SnapshotManager = exports.GlobalSnapshotsManager = void 0;
const typescript_1 = __importDefault(require("typescript"));
const DocumentSnapshot_1 = require("./DocumentSnapshot");
const logger_1 = require("../../logger");
const utils_1 = require("../../utils");
const events_1 = require("events");
const fileCollection_1 = require("../../lib/documents/fileCollection");
const path_1 = require("path");
/**
 * Every snapshot corresponds to a unique file on disk.
 * A snapshot can be part of multiple projects, but for a given file path
 * there can be only one snapshot.
 */
class GlobalSnapshotsManager {
    constructor(tsSystem, watchPackageJson = false) {
        this.tsSystem = tsSystem;
        this.emitter = new events_1.EventEmitter();
        this.documents = new fileCollection_1.FileMap(tsSystem.useCaseSensitiveFileNames);
        this.getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(tsSystem.useCaseSensitiveFileNames);
        this.packageJsonCache = new PackageJsonCache(tsSystem, watchPackageJson, this.getCanonicalFileName, this.updateSnapshotsInDirectory.bind(this));
    }
    get(fileName) {
        fileName = (0, utils_1.normalizePath)(fileName);
        return this.documents.get(fileName);
    }
    getByPrefix(path) {
        path = this.getCanonicalFileName((0, utils_1.normalizePath)(path));
        return Array.from(this.documents.entries())
            .filter((doc) => doc[0].startsWith(path))
            .map((doc) => doc[1]);
    }
    set(fileName, document) {
        fileName = (0, utils_1.normalizePath)(fileName);
        this.documents.set(fileName, document);
        this.emitter.emit('change', fileName, document);
    }
    delete(fileName) {
        fileName = (0, utils_1.normalizePath)(fileName);
        this.documents.delete(fileName);
        this.emitter.emit('change', fileName, undefined);
    }
    updateTsOrJsFile(fileName, changes) {
        fileName = (0, utils_1.normalizePath)(fileName);
        const previousSnapshot = this.get(fileName);
        if (changes) {
            if (!(previousSnapshot instanceof DocumentSnapshot_1.JSOrTSDocumentSnapshot)) {
                return;
            }
            previousSnapshot.update(changes);
            this.emitter.emit('change', fileName, previousSnapshot);
            return previousSnapshot;
        }
        else {
            const newSnapshot = DocumentSnapshot_1.DocumentSnapshot.fromNonSvelteFilePath(fileName, this.tsSystem);
            if (previousSnapshot) {
                newSnapshot.version = previousSnapshot.version + 1;
            }
            else {
                // ensure it's greater than initial version
                // so that ts server picks up the change
                newSnapshot.version += 1;
            }
            this.set(fileName, newSnapshot);
            return newSnapshot;
        }
    }
    onChange(listener) {
        this.emitter.on('change', listener);
    }
    removeChangeListener(listener) {
        this.emitter.off('change', listener);
    }
    getPackageJson(path) {
        return this.packageJsonCache.getPackageJson(path);
    }
    updateSnapshotsInDirectory(dir) {
        this.getByPrefix(dir).forEach((snapshot) => {
            this.updateTsOrJsFile(snapshot.filePath);
        });
    }
}
exports.GlobalSnapshotsManager = GlobalSnapshotsManager;
/**
 * Should only be used by `service.ts`
 */
class SnapshotManager {
    constructor(globalSnapshotsManager, fileSpec, workspaceRoot, projectFiles, useCaseSensitiveFileNames = typescript_1.default.sys.useCaseSensitiveFileNames) {
        this.globalSnapshotsManager = globalSnapshotsManager;
        this.fileSpec = fileSpec;
        this.workspaceRoot = workspaceRoot;
        this.lastLogged = new Date(new Date().getTime() - 60001);
        this.watchExtensions = [
            typescript_1.default.Extension.Dts,
            typescript_1.default.Extension.Js,
            typescript_1.default.Extension.Jsx,
            typescript_1.default.Extension.Ts,
            typescript_1.default.Extension.Tsx,
            typescript_1.default.Extension.Json
        ];
        this.onSnapshotChange = this.onSnapshotChange.bind(this);
        this.globalSnapshotsManager.onChange(this.onSnapshotChange);
        this.documents = new fileCollection_1.FileMap(useCaseSensitiveFileNames);
        this.projectFileToOriginalCasing = new Map();
        this.getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(useCaseSensitiveFileNames);
        projectFiles.forEach((originalCasing) => this.projectFileToOriginalCasing.set(this.getCanonicalFileName(originalCasing), originalCasing));
    }
    onSnapshotChange(fileName, document) {
        // Only delete/update snapshots, don't add new ones,
        // as they could be from another TS service and this
        // snapshot manager can't reach this file.
        // For these, instead wait on a `get` method invocation
        // and set them "manually" in the set/update methods.
        if (!document) {
            this.documents.delete(fileName);
            this.projectFileToOriginalCasing.delete(this.getCanonicalFileName(fileName));
        }
        else if (this.documents.has(fileName)) {
            this.documents.set(fileName, document);
        }
    }
    updateProjectFiles() {
        const { include, exclude } = this.fileSpec;
        // Since we default to not include anything,
        //  just don't waste time on this
        if (include?.length === 0) {
            return;
        }
        const projectFiles = typescript_1.default.sys
            .readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include)
            .map(utils_1.normalizePath);
        projectFiles.forEach((projectFile) => this.projectFileToOriginalCasing.set(this.getCanonicalFileName(projectFile), projectFile));
    }
    updateTsOrJsFile(fileName, changes) {
        const snapshot = this.globalSnapshotsManager.updateTsOrJsFile(fileName, changes);
        // This isn't duplicated logic to the listener, because this could
        // be a new snapshot which the listener wouldn't add.
        if (snapshot) {
            this.documents.set((0, utils_1.normalizePath)(fileName), snapshot);
        }
    }
    has(fileName) {
        fileName = (0, utils_1.normalizePath)(fileName);
        return (this.projectFileToOriginalCasing.has(this.getCanonicalFileName(fileName)) ||
            this.documents.has(fileName));
    }
    set(fileName, snapshot) {
        this.globalSnapshotsManager.set(fileName, snapshot);
        // This isn't duplicated logic to the listener, because this could
        // be a new snapshot which the listener wouldn't add.
        this.documents.set((0, utils_1.normalizePath)(fileName), snapshot);
        this.logStatistics();
    }
    get(fileName) {
        fileName = (0, utils_1.normalizePath)(fileName);
        let snapshot = this.documents.get(fileName);
        if (!snapshot) {
            snapshot = this.globalSnapshotsManager.get(fileName);
            if (snapshot) {
                this.documents.set(fileName, snapshot);
            }
        }
        return snapshot;
    }
    delete(fileName) {
        fileName = (0, utils_1.normalizePath)(fileName);
        this.globalSnapshotsManager.delete(fileName);
    }
    getFileNames() {
        return Array.from(this.documents.entries()).map(([_, doc]) => doc.filePath);
    }
    getProjectFileNames() {
        return Array.from(this.projectFileToOriginalCasing.values());
    }
    logStatistics() {
        const date = new Date();
        // Don't use setInterval because that will keep tests running forever
        if (date.getTime() - this.lastLogged.getTime() > 60000) {
            this.lastLogged = date;
            const allFiles = Array.from(new Set([...this.projectFileToOriginalCasing.keys(), ...this.documents.keys()]));
            logger_1.Logger.log('SnapshotManager File Statistics:\n' +
                `Project files: ${this.projectFileToOriginalCasing.size}\n` +
                `Svelte files: ${allFiles.filter((name) => name.endsWith('.svelte')).length}\n` +
                `From node_modules: ${allFiles.filter((name) => name.includes('node_modules')).length}\n` +
                `Total: ${allFiles.length}`);
        }
    }
    dispose() {
        this.globalSnapshotsManager.removeChangeListener(this.onSnapshotChange);
    }
}
exports.SnapshotManager = SnapshotManager;
exports.ignoredBuildDirectories = ['__sapper__', '.svelte-kit'];
class PackageJsonCache {
    constructor(tsSystem, watchPackageJson, getCanonicalFileName, updateSnapshotsInDirectory) {
        this.tsSystem = tsSystem;
        this.watchPackageJson = watchPackageJson;
        this.getCanonicalFileName = getCanonicalFileName;
        this.updateSnapshotsInDirectory = updateSnapshotsInDirectory;
        this.packageJsonCache = new fileCollection_1.FileMap();
        this.watchers = new fileCollection_1.FileMap(tsSystem.useCaseSensitiveFileNames);
    }
    getPackageJson(path) {
        if (!this.packageJsonCache.has(path)) {
            this.packageJsonCache.set(path, this.initWatcherAndRead(path));
        }
        return this.packageJsonCache.get(path);
    }
    initWatcherAndRead(path) {
        if (this.watchPackageJson) {
            this.tsSystem.watchFile?.(path, this.onPackageJsonWatchChange.bind(this), 3000);
        }
        const exist = this.tsSystem.fileExists(path);
        if (!exist) {
            return undefined;
        }
        return this.readPackageJson(path);
    }
    readPackageJson(path) {
        return {
            text: this.tsSystem.readFile(path) ?? '',
            modifiedTime: this.tsSystem.getModifiedTime?.(path)?.valueOf()
        };
    }
    onPackageJsonWatchChange(path, onWatchChange) {
        const dir = (0, path_1.dirname)(path);
        if (onWatchChange === typescript_1.default.FileWatcherEventKind.Deleted) {
            this.packageJsonCache.delete(path);
            this.watchers.get(path)?.close();
            this.watchers.delete(path);
        }
        else {
            this.packageJsonCache.set(path, this.readPackageJson(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);
            }
        }, 500);
    }
}
//# sourceMappingURL=SnapshotManager.js.map