"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SvelteSnapshotManager = exports.SvelteSnapshot = void 0;
const svelte2tsx_1 = require("svelte2tsx");
const source_mapper_1 = require("./source-mapper");
const utils_1 = require("./utils");
class SvelteSnapshot {
    constructor(typescript, fileName, svelteCode, mapper, logger, isTsFile) {
        this.typescript = typescript;
        this.fileName = fileName;
        this.svelteCode = svelteCode;
        this.mapper = mapper;
        this.logger = logger;
        this.isTsFile = isTsFile;
        this.convertInternalCodePositions = false;
    }
    update(svelteCode, mapper) {
        this.svelteCode = svelteCode;
        this.mapper = mapper;
        this.lineOffsets = undefined;
        this.log('Updated Snapshot');
    }
    getOriginalTextSpan(textSpan) {
        if (!(0, utils_1.isNoTextSpanInGeneratedCode)(this.getText(), textSpan)) {
            return null;
        }
        const start = this.getOriginalOffset(textSpan.start);
        if (start === -1) {
            return null;
        }
        // Assumption: We don't change identifiers itself, so we don't change ranges.
        return {
            start,
            length: textSpan.length
        };
    }
    getOriginalOffset(generatedOffset) {
        if (!this.scriptInfo) {
            return generatedOffset;
        }
        this.toggleMappingMode(true);
        const lineOffset = this.scriptInfo.positionToLineOffset(generatedOffset);
        this.debug('try convert offset', generatedOffset, '/', lineOffset);
        const original = this.mapper.getOriginalPosition({
            line: lineOffset.line - 1,
            character: lineOffset.offset - 1
        });
        this.toggleMappingMode(false);
        if (original.line === -1) {
            return -1;
        }
        const originalOffset = this.scriptInfo.lineOffsetToPosition(original.line + 1, original.character + 1);
        this.debug('converted offset to', original, '/', originalOffset);
        return originalOffset;
    }
    getGeneratedTextSpan(textSpan) {
        const start = this.getGeneratedOffset(textSpan.start);
        if (start === -1) {
            return null;
        }
        // Assumption: We don't change identifiers itself, so we don't change ranges.
        return {
            start,
            length: textSpan.length
        };
    }
    getGeneratedOffset(originalOffset) {
        if (!this.scriptInfo) {
            return originalOffset;
        }
        const lineOffset = this.scriptInfo.positionToLineOffset(originalOffset);
        const original = this.mapper.getGeneratedPosition({
            line: lineOffset.line - 1,
            character: lineOffset.offset - 1
        });
        if (original.line === -1) {
            return -1;
        }
        this.toggleMappingMode(true);
        const generatedOffset = this.scriptInfo.lineOffsetToPosition(original.line + 1, original.character + 1);
        this.toggleMappingMode(false);
        this.debug('converted offset to', original, '/', generatedOffset);
        return generatedOffset;
    }
    setAndPatchScriptInfo(scriptInfo) {
        // @ts-expect-error
        scriptInfo.scriptKind = this.typescript.ScriptKind.TS;
        const positionToLineOffset = scriptInfo.positionToLineOffset.bind(scriptInfo);
        scriptInfo.positionToLineOffset = (position) => {
            if (this.convertInternalCodePositions) {
                const lineOffset = positionToLineOffset(position);
                this.debug('positionToLineOffset for generated code', position, lineOffset);
                return lineOffset;
            }
            const lineOffset = this.positionAt(position);
            this.debug('positionToLineOffset for original code', position, lineOffset);
            return { line: lineOffset.line + 1, offset: lineOffset.character + 1 };
        };
        const lineOffsetToPosition = scriptInfo.lineOffsetToPosition.bind(scriptInfo);
        scriptInfo.lineOffsetToPosition = (line, offset) => {
            if (this.convertInternalCodePositions) {
                const position = lineOffsetToPosition(line, offset);
                this.debug('lineOffsetToPosition for generated code', { line, offset }, position);
                return position;
            }
            const position = this.offsetAt({ line: line - 1, character: offset - 1 });
            this.debug('lineOffsetToPosition for original code', { line, offset }, position);
            return position;
        };
        // TODO do we need to patch this?
        // const lineToTextSpan = scriptInfo.lineToTextSpan.bind(scriptInfo);
        // scriptInfo.lineToTextSpan = (line) => {
        //     if (this.convertInternalCodePositions) {
        //         const span = lineToTextSpan(line);
        //         this.debug('lineToTextSpan for generated code', line, span);
        //         return span;
        //     }
        //     const lineOffset = this.getLineOffsets();
        //     const start = lineOffset[line - 1];
        //     const span: ts.TextSpan = {
        //         start,
        //         length: (lineOffset[line] || this.svelteCode.length) - start
        //     };
        //     this.debug('lineToTextSpan for original code', line, span);
        //     return span;
        // };
        this.scriptInfo = scriptInfo;
        this.log('patched scriptInfo');
    }
    /**
     * Get the line and character based on the offset
     * @param offset The index of the position
     */
    positionAt(offset) {
        offset = this.clamp(offset, 0, this.svelteCode.length);
        const lineOffsets = this.getLineOffsets();
        let low = 0;
        let high = lineOffsets.length;
        if (high === 0) {
            return { line: 0, character: offset };
        }
        while (low < high) {
            const mid = Math.floor((low + high) / 2);
            if (lineOffsets[mid] > offset) {
                high = mid;
            }
            else {
                low = mid + 1;
            }
        }
        // low is the least x for which the line offset is larger than the current offset
        // or array.length if no line offset is larger than the current offset
        const line = low - 1;
        return { line, character: offset - lineOffsets[line] };
    }
    /**
     * Get the index of the line and character position
     * @param position Line and character position
     */
    offsetAt(position) {
        const lineOffsets = this.getLineOffsets();
        if (position.line >= lineOffsets.length) {
            return this.svelteCode.length;
        }
        else if (position.line < 0) {
            return 0;
        }
        const lineOffset = lineOffsets[position.line];
        const nextLineOffset = position.line + 1 < lineOffsets.length
            ? lineOffsets[position.line + 1]
            : this.svelteCode.length;
        return this.clamp(nextLineOffset, lineOffset, lineOffset + position.character);
    }
    getLineOffsets() {
        if (this.lineOffsets) {
            return this.lineOffsets;
        }
        const lineOffsets = [];
        const text = this.svelteCode;
        let isLineStart = true;
        for (let i = 0; i < text.length; i++) {
            if (isLineStart) {
                lineOffsets.push(i);
                isLineStart = false;
            }
            const ch = text.charAt(i);
            isLineStart = ch === '\r' || ch === '\n';
            if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
                i++;
            }
        }
        if (isLineStart && text.length > 0) {
            lineOffsets.push(text.length);
        }
        this.lineOffsets = lineOffsets;
        return lineOffsets;
    }
    clamp(num, min, max) {
        return Math.max(min, Math.min(max, num));
    }
    log(...args) {
        this.logger.log('SvelteSnapshot:', this.fileName, '-', ...args);
    }
    debug(...args) {
        this.logger.debug('SvelteSnapshot:', this.fileName, '-', ...args);
    }
    toggleMappingMode(convertInternalCodePositions) {
        this.convertInternalCodePositions = convertInternalCodePositions;
    }
    getText() {
        var _a;
        const snapshot = (_a = this.scriptInfo) === null || _a === void 0 ? void 0 : _a.getSnapshot();
        if (!snapshot) {
            return '';
        }
        return snapshot.getText(0, snapshot.getLength());
    }
    getOriginalText() {
        return this.svelteCode;
    }
}
exports.SvelteSnapshot = SvelteSnapshot;
class SvelteSnapshotManager {
    constructor(typescript, projectService, svelteOptions, logger, configManager) {
        this.typescript = typescript;
        this.projectService = projectService;
        this.svelteOptions = svelteOptions;
        this.logger = logger;
        this.configManager = configManager;
        this.snapshots = new Map();
        this.patchProjectServiceReadFile();
    }
    get(fileName) {
        return this.snapshots.get(this.projectService.toCanonicalFileName(fileName));
    }
    create(fileName) {
        const canonicalFilePath = this.projectService.toCanonicalFileName(fileName);
        if (this.snapshots.has(canonicalFilePath)) {
            return this.snapshots.get(canonicalFilePath);
        }
        // This will trigger projectService.host.readFile which is patched below
        const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(this.typescript.server.toNormalizedPath(fileName), false);
        if (!scriptInfo) {
            this.logger.log('Was not able get snapshot for', fileName);
            return;
        }
        try {
            scriptInfo.getSnapshot(); // needed to trigger readFile
        }
        catch (e) {
            this.logger.log('Loading Snapshot failed', fileName);
        }
        const snapshot = this.snapshots.get(this.projectService.toCanonicalFileName(fileName));
        if (!snapshot) {
            this.logger.log('Svelte snapshot was not found after trying to load script snapshot for', fileName);
            return; // should never get here
        }
        snapshot.setAndPatchScriptInfo(scriptInfo);
        this.snapshots.set(canonicalFilePath, snapshot);
        return snapshot;
    }
    patchProjectServiceReadFile() {
        // @ts-ignore The projectService is shared across some instances, make sure we patch readFile only once
        if (!this.projectService.host[onReadSvelteFile]) {
            this.logger.log('patching projectService host readFile');
            // @ts-ignore
            this.projectService.host[onReadSvelteFile] = [];
            const readFile = this.projectService.host.readFile;
            this.projectService.host.readFile = (path, encoding) => {
                if (!this.configManager.getConfig().enable) {
                    return readFile(path, encoding);
                }
                // The following (very hacky) first two checks make sure that the ambient module definitions
                // that tell TS "every import ending with .svelte is a valid module" are removed.
                // They exist in svelte2tsx and svelte to make sure that people don't
                // get errors in their TS files when importing Svelte files and not using our TS plugin.
                // If someone wants to get back the behavior they can add an ambient module definition
                // on their own.
                const normalizedPath = path.replace(/\\/g, '/');
                if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
                    return '';
                }
                else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) {
                    // Remove the dom lib reference to not load these ambient types in case
                    // the user has a tsconfig.json with different lib settings like in
                    // https://github.com/sveltejs/language-tools/issues/1733
                    const originalText = readFile(path) || '';
                    const toReplace = '/// <reference lib="dom" />';
                    return originalText.replace(toReplace, ' '.repeat(toReplace.length));
                }
                else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) {
                    let originalText = readFile(path) || '';
                    if (!originalText.includes('// -- start svelte-ls-remove --')) {
                        return originalText; // uses an older version of svelte2tsx or is already patched
                    }
                    const startIdx = originalText.indexOf('// -- start svelte-ls-remove --');
                    const endIdx = originalText.indexOf('// -- end svelte-ls-remove --');
                    originalText =
                        originalText.substring(0, startIdx) +
                            ' '.repeat(endIdx - startIdx) +
                            originalText.substring(endIdx);
                    return originalText;
                }
                else if ((0, utils_1.isSvelteFilePath)(path)) {
                    this.logger.debug('Read Svelte file:', path);
                    const svelteCode = readFile(path) || '';
                    const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js.
                    let code;
                    let mapper;
                    try {
                        const result = (0, svelte2tsx_1.svelte2tsx)(svelteCode, {
                            filename: path.split('/').pop(),
                            isTsFile,
                            mode: 'ts',
                            typingsNamespace: this.svelteOptions.namespace
                        });
                        code = result.code;
                        mapper = new source_mapper_1.SourceMapper(result.map.mappings);
                        this.logger.log('Successfully read Svelte file contents of', path);
                    }
                    catch (e) {
                        this.logger.log('Error loading Svelte file:', path, ' Using fallback.');
                        this.logger.debug('Error:', e);
                        // Return something either way, else "X is not a module" errors will appear
                        // in the TS files that use this file.
                        code = 'export default class extends Svelte2TsxComponent<any,any,any> {}';
                        mapper = new source_mapper_1.SourceMapper('');
                    }
                    // @ts-ignore
                    this.projectService.host[onReadSvelteFile].forEach((listener) => listener(path, svelteCode, isTsFile, mapper));
                    return code;
                }
                else {
                    return readFile(path, encoding);
                }
            };
        }
        // @ts-ignore
        this.projectService.host[onReadSvelteFile].push((path, svelteCode, isTsFile, mapper) => {
            const canonicalFilePath = this.projectService.toCanonicalFileName(path);
            const existingSnapshot = this.snapshots.get(canonicalFilePath);
            if (existingSnapshot) {
                existingSnapshot.update(svelteCode, mapper);
            }
            else {
                this.snapshots.set(canonicalFilePath, new SvelteSnapshot(this.typescript, path, svelteCode, mapper, this.logger, isTsFile));
            }
        });
    }
}
exports.SvelteSnapshotManager = SvelteSnapshotManager;
const onReadSvelteFile = Symbol('sveltePluginPatchSymbol');
//# sourceMappingURL=svelte-snapshots.js.map