"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSvelteModuleLoader = createSvelteModuleLoader;
const typescript_1 = __importDefault(require("typescript"));
const fileCollection_1 = require("../../lib/documents/fileCollection");
const utils_1 = require("../../utils");
const svelte_sys_1 = require("./svelte-sys");
const utils_2 = require("./utils");
const CACHE_KEY_SEPARATOR = ':::';
/**
 * Caches resolved modules.
 */
class ModuleResolutionCache {
    constructor() {
        this.cache = new fileCollection_1.FileMap();
        this.pendingInvalidations = new fileCollection_1.FileSet();
        this.getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames);
    }
    /**
     * Tries to get a cached module.
     * Careful: `undefined` can mean either there's no match found, or that the result resolved to `undefined`.
     */
    get(moduleName, containingFile) {
        return this.cache.get(this.getKey(moduleName, containingFile));
    }
    /**
     * Checks if has cached module.
     */
    has(moduleName, containingFile) {
        return this.cache.has(this.getKey(moduleName, containingFile));
    }
    /**
     * Caches resolved module (or undefined).
     */
    set(moduleName, containingFile, resolvedModule) {
        this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
    }
    /**
     * Deletes module from cache. Call this if a file was deleted.
     * @param resolvedModuleName full path of the module
     */
    delete(resolvedModuleName) {
        resolvedModuleName = this.getCanonicalFileName(resolvedModuleName);
        this.cache.forEach((val, key) => {
            if (val.resolvedModule &&
                this.getCanonicalFileName(val.resolvedModule.resolvedFileName) ===
                    resolvedModuleName) {
                this.cache.delete(key);
                this.pendingInvalidations.add(key.split(CACHE_KEY_SEPARATOR).shift() || '');
            }
        });
    }
    /**
     * Deletes everything from cache that resolved to `undefined`
     * and which might match the path.
     */
    deleteByValues(list) {
        this.cache.forEach((val, key) => {
            if (list.includes(val)) {
                this.cache.delete(key);
            }
        });
    }
    getKey(moduleName, containingFile) {
        return containingFile + CACHE_KEY_SEPARATOR + (0, utils_2.ensureRealSvelteFilePath)(moduleName);
    }
    clearPendingInvalidations() {
        this.pendingInvalidations.clear();
    }
    oneOfResolvedModuleChanged(path) {
        return this.pendingInvalidations.size > 0 && this.pendingInvalidations.has(path);
    }
}
class ImpliedNodeFormatResolver {
    constructor(tsSystem) {
        this.tsSystem = tsSystem;
    }
    resolve(importPath, importIdxInFile, sourceFile, compilerOptions) {
        if ((0, utils_2.isSvelteFilePath)(importPath)) {
            // Svelte imports should use the old resolution algorithm, else they are not found
            return undefined;
        }
        let mode = undefined;
        if (sourceFile) {
            mode = typescript_1.default.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);
        }
        return mode;
    }
    resolveForTypeReference(entry, sourceFile) {
        let mode = undefined;
        if (sourceFile) {
            mode = typescript_1.default.getModeForFileReference(entry, sourceFile?.impliedNodeFormat);
        }
        return mode;
    }
}
// https://github.com/microsoft/TypeScript/blob/dddd0667f012c51582c2ac92c08b8e57f2456587/src/compiler/program.ts#L989
function getTypeReferenceResolutionName(entry) {
    return typeof entry !== 'string' ? (0, utils_1.toFileNameLowerCase)(entry.fileName) : entry;
}
/**
 * Creates a module loader specifically for `.svelte` files.
 *
 * The typescript language service tries to look up other files that are referenced in the currently open svelte file.
 * For `.ts`/`.js` files this works, for `.svelte` files it does not by default.
 * Reason: The typescript language service does not know about the `.svelte` file ending,
 * so it assumes it's a normal typescript file and searches for files like `../Component.svelte.ts`, which is wrong.
 * In order to fix this, we need to wrap typescript's module resolution and reroute all `.svelte.ts` file lookups to .svelte.
 *
 * @param getSnapshot A function which returns a (in case of svelte file fully preprocessed) typescript/javascript snapshot
 * @param compilerOptions The typescript compiler options
 */
function createSvelteModuleLoader(getSnapshot, compilerOptions, tsSystem, tsModule, getModuleResolutionHost) {
    const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(tsSystem.useCaseSensitiveFileNames);
    const svelteSys = (0, svelte_sys_1.createSvelteSys)(tsSystem);
    // tsModuleCache caches package.json parsing and module resolution for directory
    const tsModuleCache = tsModule.createModuleResolutionCache(tsSystem.getCurrentDirectory(), (0, utils_1.createGetCanonicalFileName)(tsSystem.useCaseSensitiveFileNames));
    const tsTypeReferenceDirectiveCache = tsModule.createTypeReferenceDirectiveResolutionCache(tsSystem.getCurrentDirectory(), getCanonicalFileName, undefined, tsModuleCache.getPackageJsonInfoCache());
    const moduleCache = new ModuleResolutionCache();
    const typeReferenceCache = new Map();
    const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(tsSystem);
    const resolutionWithFailedLookup = new Set();
    const failedLocationInvalidated = new fileCollection_1.FileSet(tsSystem.useCaseSensitiveFileNames);
    const pendingFailedLocationCheck = new fileCollection_1.FileSet(tsSystem.useCaseSensitiveFileNames);
    return {
        svelteFileExists: svelteSys.svelteFileExists,
        fileExists: svelteSys.fileExists,
        readFile: svelteSys.readFile,
        readDirectory: svelteSys.readDirectory,
        deleteFromModuleCache: (path) => {
            svelteSys.deleteFromCache(path);
            moduleCache.delete(path);
        },
        scheduleResolutionFailedLocationCheck: (path) => {
            svelteSys.deleteFromCache(path);
            if ((0, utils_2.isSvelteFilePath)(path)) {
                pendingFailedLocationCheck.add((0, utils_2.toVirtualSvelteFilePath)(path));
            }
            else {
                pendingFailedLocationCheck.add(path);
            }
        },
        resolveModuleNames,
        resolveTypeReferenceDirectiveReferences,
        mightHaveInvalidatedResolutions,
        clearPendingInvalidations,
        getModuleResolutionCache: () => tsModuleCache,
        invalidateFailedLocationResolution
    };
    function resolveModuleNames(moduleNames, containingFile, _reusedNames, redirectedReference, options, containingSourceFile) {
        return moduleNames.map((moduleName, index) => {
            const cached = moduleCache.get(moduleName, containingFile);
            if (cached) {
                return cached.resolvedModule;
            }
            const resolvedModule = resolveModuleName(moduleName, containingFile, containingSourceFile, index, redirectedReference, options);
            cacheResolutionWithFailedLookup(resolvedModule, containingFile);
            moduleCache.set(moduleName, containingFile, resolvedModule);
            return resolvedModule?.resolvedModule;
        });
    }
    function resolveModuleName(name, containingFile, containingSourceFile, index, redirectedReference, option) {
        const mode = impliedNodeFormatResolver.resolve(name, index, containingSourceFile, 
        // use the same compiler options as resolveModuleName
        // otherwise it might not find the module because of inconsistent module resolution strategy
        redirectedReference?.commandLine.options ?? option);
        const resolvedModuleWithFailedLookup = tsModule.resolveModuleName(name, containingFile, compilerOptions, getModuleResolutionHost() ?? svelteSys, tsModuleCache, redirectedReference, mode);
        const resolvedModule = resolvedModuleWithFailedLookup.resolvedModule;
        if (!resolvedModule || !(0, utils_2.isVirtualSvelteFilePath)(resolvedModule.resolvedFileName)) {
            return resolvedModuleWithFailedLookup;
        }
        const resolvedFileName = svelteSys.getRealSveltePathIfExists(resolvedModule.resolvedFileName);
        if (!(0, utils_2.isSvelteFilePath)(resolvedFileName)) {
            return resolvedModuleWithFailedLookup;
        }
        const snapshot = getSnapshot(resolvedFileName);
        const resolvedSvelteModule = {
            extension: (0, utils_2.getExtensionFromScriptKind)(snapshot && snapshot.scriptKind),
            resolvedFileName,
            isExternalLibraryImport: resolvedModule.isExternalLibraryImport
        };
        return {
            ...resolvedModuleWithFailedLookup,
            resolvedModule: resolvedSvelteModule
        };
    }
    function resolveTypeReferenceDirectiveReferences(typeDirectiveNames, containingFile, redirectedReference, options, containingSourceFile) {
        return typeDirectiveNames.map((typeDirectiveName) => {
            const entry = getTypeReferenceResolutionName(typeDirectiveName);
            const mode = impliedNodeFormatResolver.resolveForTypeReference(entry, containingSourceFile);
            const key = `${entry}|${mode}`;
            let result = typeReferenceCache.get(key);
            if (!result) {
                result = typescript_1.default.resolveTypeReferenceDirective(entry, containingFile, options, {
                    ...tsSystem
                }, redirectedReference, tsTypeReferenceDirectiveCache, mode);
                typeReferenceCache.set(key, result);
            }
            return result;
        });
    }
    function mightHaveInvalidatedResolutions(path) {
        return (moduleCache.oneOfResolvedModuleChanged(path) ||
            // tried but failed file might now exist
            (failedLocationInvalidated.size > 0 && failedLocationInvalidated.has(path)));
    }
    function clearPendingInvalidations() {
        moduleCache.clearPendingInvalidations();
        failedLocationInvalidated.clear();
        pendingFailedLocationCheck.clear();
    }
    function cacheResolutionWithFailedLookup(resolvedModule, containingFile) {
        if (!resolvedModule.failedLookupLocations?.length) {
            return;
        }
        // The resolvedModule object will be reused in different files. A bit hacky, but TypeScript also does this.
        // https://github.com/microsoft/TypeScript/blob/11e79327598db412a161616849041487673fadab/src/compiler/resolutionCache.ts#L1103
        resolvedModule.files ??= new Set();
        resolvedModule.files.add(containingFile);
        resolutionWithFailedLookup.add(resolvedModule);
    }
    function invalidateFailedLocationResolution() {
        if (pendingFailedLocationCheck.size === 0) {
            return;
        }
        const toRemoves = [];
        resolutionWithFailedLookup.forEach((resolvedModule) => {
            if (!resolvedModule.failedLookupLocations) {
                return;
            }
            for (const location of resolvedModule.failedLookupLocations) {
                if (pendingFailedLocationCheck.has(location)) {
                    resolutionWithFailedLookup.delete(resolvedModule);
                    toRemoves.push(resolvedModule);
                    resolvedModule.files?.forEach((file) => {
                        failedLocationInvalidated.add(file);
                    });
                    break;
                }
            }
        });
        if (toRemoves.length) {
            moduleCache.deleteByValues(toRemoves);
            tsModuleCache.clear();
            tsTypeReferenceDirectiveCache.clear();
        }
        pendingFailedLocationCheck.clear();
    }
}
//# sourceMappingURL=module-loader.js.map