"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CallHierarchyProviderImpl = void 0;
const path_1 = __importStar(require("path"));
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../lib/documents");
const utils_1 = require("../../../utils");
const DocumentSnapshot_1 = require("../DocumentSnapshot");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
const ENSURE_COMPONENT_HELPER = '__sveltets_2_ensureComponent';
class CallHierarchyProviderImpl {
    constructor(lsAndTsDocResolver, workspaceUris) {
        this.lsAndTsDocResolver = lsAndTsDocResolver;
        this.workspaceUris = workspaceUris;
    }
    async prepareCallHierarchy(document, position, cancellationToken) {
        const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return null;
        }
        const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
        const items = lang.prepareCallHierarchy(tsDoc.filePath, offset);
        const itemsArray = Array.isArray(items) ? items : items ? [items] : [];
        const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver);
        snapshots.set(tsDoc.filePath, tsDoc);
        const program = lang.getProgram();
        const result = await Promise.all(itemsArray.map((item) => this.convertCallHierarchyItem(snapshots, item, program)));
        return result.filter(utils_1.isNotNullOrUndefined);
    }
    isSourceFileItem(item) {
        return (item.kind === typescript_1.default.ScriptElementKind.scriptElement ||
            (item.kind === typescript_1.default.ScriptElementKind.moduleElement && item.selectionSpan.start === 0));
    }
    async convertCallHierarchyItem(snapshots, item, program) {
        const snapshot = await snapshots.retrieve(item.file);
        const redirectedCallHierarchyItem = this.redirectCallHierarchyItem(snapshot, program, item);
        if (redirectedCallHierarchyItem) {
            return redirectedCallHierarchyItem;
        }
        const { name, detail } = this.getNameAndDetailForItem(this.isSourceFileItem(item), item);
        const selectionRange = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, item.selectionSpan));
        if (selectionRange.start.line < 0 || selectionRange.end.line < 0) {
            return null;
        }
        const range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, item.span));
        if (range.start.line < 0 || range.end.line < 0) {
            return null;
        }
        return {
            kind: (0, utils_2.symbolKindFromString)(item.kind),
            name,
            range,
            selectionRange,
            uri: (0, utils_1.pathToUrl)(item.file),
            detail,
            tags: item.kindModifiers?.includes('deprecated') ? [vscode_languageserver_1.SymbolTag.Deprecated] : undefined
        };
    }
    getNameAndDetailForItem(useFileName, item) {
        const nearestRootUri = (0, utils_2.getNearestWorkspaceUri)(this.workspaceUris, item.file, (0, utils_1.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames));
        const nearestRoot = nearestRootUri && ((0, utils_1.urlToPath)(nearestRootUri) ?? undefined);
        const name = useFileName ? (0, path_1.basename)(item.file) : item.name;
        const detail = useFileName
            ? nearestRoot && path_1.default.relative(nearestRoot, (0, path_1.dirname)(item.file))
            : item.containerName;
        return { name, detail };
    }
    async getIncomingCalls(previousItem, cancellationToken) {
        const prepareResult = await this.prepareFurtherCalls(previousItem, cancellationToken);
        if (!prepareResult) {
            return null;
        }
        const { lang, filePath, program, snapshots, isComponentModulePosition, tsDoc, getNonComponentOffset } = prepareResult;
        const componentExportOffset = isComponentModulePosition && tsDoc instanceof DocumentSnapshot_1.SvelteDocumentSnapshot
            ? (0, utils_2.offsetOfGeneratedComponentExport)(tsDoc)
            : -1;
        const offset = componentExportOffset >= 0 ? componentExportOffset : getNonComponentOffset();
        const incomingCalls = lang
            .provideCallHierarchyIncomingCalls(filePath, offset)
            .concat(this.getInComingCallsForComponent(lang, program, filePath, offset) ?? []);
        const result = await Promise.all(incomingCalls.map(async (item) => {
            const snapshot = await snapshots.retrieve(item.from.file);
            const from = await this.convertCallHierarchyItem(snapshots, item.from, program);
            if (!from) {
                return null;
            }
            return {
                from,
                fromRanges: this.convertFromRanges(snapshot, item.fromSpans)
            };
        }));
        return result.filter(utils_1.isNotNullOrUndefined);
    }
    async getOutgoingCalls(previousItem, cancellationToken) {
        const prepareResult = await this.prepareFurtherCalls(previousItem, cancellationToken);
        if (!prepareResult) {
            return null;
        }
        const { lang, filePath, program, snapshots, isComponentModulePosition, tsDoc, getNonComponentOffset } = prepareResult;
        const sourceFile = program?.getSourceFile(filePath);
        const renderFunctionOffset = isComponentModulePosition && tsDoc instanceof DocumentSnapshot_1.SvelteDocumentSnapshot && sourceFile
            ? sourceFile.statements
                .find((statement) => typescript_1.default.isFunctionDeclaration(statement) &&
                statement.name?.getText() === 'render')
                ?.name?.getStart()
            : -1;
        const offset = renderFunctionOffset != null && renderFunctionOffset >= 0
            ? renderFunctionOffset
            : getNonComponentOffset();
        const outgoingCalls = lang
            .provideCallHierarchyOutgoingCalls(filePath, offset)
            .concat(isComponentModulePosition
            ? this.getOutgoingCallsForComponent(program, filePath) ?? []
            : []);
        const result = await Promise.all(outgoingCalls.map(async (item) => {
            if (item.to.name.startsWith('__sveltets') ||
                item.to.containerName === 'svelteHTML') {
                return null;
            }
            const to = await this.convertCallHierarchyItem(snapshots, item.to, program);
            if (!to) {
                return null;
            }
            return {
                to,
                fromRanges: this.convertFromRanges(tsDoc, item.fromSpans)
            };
        }));
        return result.filter(utils_1.isNotNullOrUndefined).filter((item) => item.fromRanges.length);
    }
    async prepareFurtherCalls(item, cancellationToken) {
        const filePath = (0, utils_1.urlToPath)(item.uri);
        if (!filePath) {
            return null;
        }
        const lang = await this.lsAndTsDocResolver.getLSForPath(filePath);
        const tsDoc = await this.lsAndTsDocResolver.getSnapshot(filePath);
        if (cancellationToken?.isCancellationRequested) {
            return null;
        }
        const program = lang.getProgram();
        const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver);
        snapshots.set(tsDoc.filePath, tsDoc);
        const isComponentModulePosition = (0, utils_2.isSvelteFilePath)(item.name) &&
            item.selectionRange.start.line === 0 &&
            item.range.start.line === 0;
        return {
            snapshots,
            filePath,
            program,
            tsDoc,
            lang,
            isComponentModulePosition,
            getNonComponentOffset: () => tsDoc.offsetAt(tsDoc.getGeneratedPosition(item.selectionRange.start))
        };
    }
    redirectCallHierarchyItem(snapshot, program, item) {
        if (!(0, utils_2.isSvelteFilePath)(item.file) ||
            !program ||
            !(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
            return null;
        }
        const sourceFile = program.getSourceFile(item.file);
        if (!sourceFile) {
            return null;
        }
        if ((0, utils_2.isGeneratedSvelteComponentName)(item.name)) {
            return this.toComponentCallHierarchyItem(snapshot, item);
        }
        if (item.name === 'render') {
            const end = item.selectionSpan.start + item.selectionSpan.length;
            const renderFunction = sourceFile.statements.find((statement) => statement.getStart() <= item.selectionSpan.start && statement.getEnd() >= end);
            if (!renderFunction || !sourceFile.statements.includes(renderFunction)) {
                return null;
            }
            return this.toComponentCallHierarchyItem(snapshot, item);
        }
        return null;
    }
    toComponentCallHierarchyItem(snapshot, item) {
        const fileStartPosition = vscode_languageserver_types_1.Position.create(0, 0);
        const fileRange = vscode_languageserver_1.Range.create(fileStartPosition, snapshot.parent.positionAt(snapshot.parent.getTextLength()));
        return {
            ...this.getNameAndDetailForItem(true, item),
            kind: vscode_languageserver_1.SymbolKind.Module,
            range: fileRange,
            selectionRange: vscode_languageserver_1.Range.create(fileStartPosition, fileStartPosition),
            uri: (0, utils_1.pathToUrl)(item.file)
        };
    }
    convertFromRanges(snapshot, spans) {
        return spans
            .map((item) => (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, item)))
            .filter((range) => range.start.line >= 0 && range.end.line >= 0);
    }
    getInComingCallsForComponent(lang, program, filePath, offset) {
        if (!program || !(0, utils_2.isSvelteFilePath)(filePath)) {
            return null;
        }
        const groups = lang
            .findReferences(filePath, offset)
            ?.map((entry) => [
            entry.definition.fileName,
            entry.references
                .map((ref) => this.getComponentStartTagFromReference(program, ref))
                .filter(utils_1.isNotNullOrUndefined)
        ])
            .filter(([_, group]) => group.length);
        return (groups?.map(([file, group]) => ({
            from: {
                file,
                kind: typescript_1.default.ScriptElementKind.scriptElement,
                name: (0, utils_2.toGeneratedSvelteComponentName)(''),
                // doesn't matter, will be override later
                selectionSpan: { start: 0, length: 0 },
                span: { start: 0, length: 0 }
            },
            fromSpans: group.map((g) => g.textSpan)
        })) ?? null);
    }
    getComponentStartTagFromReference(program, ref) {
        const sourceFile = program.getSourceFile(ref.fileName);
        if (!sourceFile) {
            return null;
        }
        const node = (0, utils_3.findNodeAtSpan)(sourceFile, ref.textSpan, this.isComponentStartTag);
        if (node) {
            return ref;
        }
        return null;
    }
    isComponentStartTag(node) {
        return (!!node &&
            node.parent &&
            typescript_1.default.isCallExpression(node.parent) &&
            typescript_1.default.isIdentifier(node.parent.expression) &&
            node.parent.expression.text === ENSURE_COMPONENT_HELPER &&
            typescript_1.default.isIdentifier(node) &&
            node === node.parent.arguments[0]);
    }
    getOutgoingCallsForComponent(program, filePath) {
        const sourceFile = program?.getSourceFile(filePath);
        if (!program || !sourceFile) {
            return null;
        }
        const groups = new Map();
        const startTags = (0, utils_3.gatherDescendants)(sourceFile, this.isComponentStartTag);
        const typeChecker = program.getTypeChecker();
        for (const startTag of startTags) {
            const type = typeChecker.getTypeAtLocation(startTag);
            const symbol = type.aliasSymbol ?? type.symbol;
            const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
            if (!declaration || !typescript_1.default.isClassDeclaration(declaration)) {
                continue;
            }
            let group = groups.get(declaration);
            if (!group) {
                group = [];
                groups.set(declaration, group);
            }
            group.push({ start: startTag.getStart(), length: startTag.getWidth() });
        }
        return (Array.from(groups).map(([declaration, group]) => {
            const file = declaration.getSourceFile().fileName;
            const name = declaration.name?.getText() ?? (0, path_1.basename)(file);
            const span = { start: declaration.getStart(), length: declaration.getWidth() };
            const selectionSpan = declaration.name
                ? { start: declaration.name.getStart(), length: declaration.name.getWidth() }
                : span;
            return {
                to: {
                    file,
                    kind: typescript_1.default.ScriptElementKind.classElement,
                    name,
                    selectionSpan,
                    span
                },
                fromSpans: group
            };
        }) ?? null);
    }
}
exports.CallHierarchyProviderImpl = CallHierarchyProviderImpl;
//# sourceMappingURL=CallHierarchyProvider.js.map