"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MdReferencesProvider = exports.MdReferenceKind = void 0;
const logging_1 = require("../logging");
const position_1 = require("../types/position");
const range_1 = require("../types/range");
const textDocument_1 = require("../types/textDocument");
const dispose_1 = require("../util/dispose");
const file_1 = require("../util/file");
const workspace_1 = require("../workspace");
const documentLinks_1 = require("./documentLinks");
var MdReferenceKind;
(function (MdReferenceKind) {
    MdReferenceKind[MdReferenceKind["Link"] = 1] = "Link";
    MdReferenceKind[MdReferenceKind["Header"] = 2] = "Header";
})(MdReferenceKind = exports.MdReferenceKind || (exports.MdReferenceKind = {}));
/**
 * Stateful object that computes references for markdown files.
 */
class MdReferencesProvider extends dispose_1.Disposable {
    #configuration;
    #parser;
    #workspace;
    #tocProvider;
    #linkCache;
    #logger;
    constructor(configuration, parser, workspace, tocProvider, linkCache, logger) {
        super();
        this.#configuration = configuration;
        this.#parser = parser;
        this.#workspace = workspace;
        this.#tocProvider = tocProvider;
        this.#linkCache = linkCache;
        this.#logger = logger;
    }
    async provideReferences(document, position, context, token) {
        const allRefs = await this.getReferencesAtPosition(document, position, token);
        return allRefs
            .filter(ref => context.includeDeclaration || !ref.isDefinition)
            .map(ref => ref.location);
    }
    async getReferencesAtPosition(document, position, token) {
        this.#logger.log(logging_1.LogLevel.Debug, 'ReferencesProvider.getReferencesAtPosition', { document: document.uri, version: document.version });
        const toc = await this.#tocProvider.getForDocument(document);
        if (token.isCancellationRequested) {
            return [];
        }
        const header = toc.entries.find(entry => entry.line === position.line);
        if (header) {
            return this.#getReferencesToHeader(document, header, token);
        }
        else {
            return this.#getReferencesToLinkAtPosition(document, position, token);
        }
    }
    async getReferencesToFileInWorkspace(resource, token) {
        this.#logger.log(logging_1.LogLevel.Debug, 'ReferencesProvider.getAllReferencesToFileInWorkspace', { resource });
        const allLinksInWorkspace = await this.#getAllLinksInWorkspace();
        if (token.isCancellationRequested) {
            return [];
        }
        return Array.from(this.#findLinksToFile(resource, allLinksInWorkspace, undefined));
    }
    async #getReferencesToHeader(document, header, token) {
        const links = await this.#getAllLinksInWorkspace();
        if (token.isCancellationRequested) {
            return [];
        }
        const references = [];
        references.push({
            kind: MdReferenceKind.Header,
            isTriggerLocation: true,
            isDefinition: true,
            location: header.headerLocation,
            headerText: header.text,
            headerTextLocation: header.headerTextLocation
        });
        for (const link of links) {
            if (link.href.kind === documentLinks_1.HrefKind.Internal
                && (0, documentLinks_1.looksLikeLinkToResource)(this.#configuration, link.href, (0, textDocument_1.getDocUri)(document))
                && this.#parser.slugifier.fromHeading(link.href.fragment).value === header.slug.value) {
                references.push({
                    kind: MdReferenceKind.Link,
                    isTriggerLocation: false,
                    isDefinition: false,
                    link,
                    location: { uri: link.source.resource.toString(), range: link.source.hrefRange },
                });
            }
        }
        return references;
    }
    async #getReferencesToLinkAtPosition(document, position, token) {
        const docLinks = (await this.#linkCache.getForDocs([document]))[0];
        if (token.isCancellationRequested) {
            return [];
        }
        for (const link of docLinks) {
            if (link.kind === documentLinks_1.MdLinkKind.Definition) {
                // We could be in either the ref name or the definition
                if ((0, range_1.rangeContains)(link.ref.range, position)) {
                    return Array.from(this.#getReferencesToLinkReference(docLinks, link.ref.text, { resource: (0, textDocument_1.getDocUri)(document), range: link.ref.range }));
                }
                else if ((0, range_1.rangeContains)(link.source.hrefRange, position)) {
                    return this.#getReferencesToLink(docLinks, link, position, token);
                }
            }
            else {
                if ((0, range_1.rangeContains)(link.source.hrefRange, position)) {
                    return this.#getReferencesToLink(docLinks, link, position, token);
                }
            }
        }
        return [];
    }
    async #getReferencesToLink(docLinks, sourceLink, triggerPosition, token) {
        if (sourceLink.href.kind === documentLinks_1.HrefKind.Reference) {
            return Array.from(this.#getReferencesToLinkReference(docLinks, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange }));
        }
        // Otherwise find all occurrences of the link in the workspace
        const allLinksInWorkspace = await this.#getAllLinksInWorkspace();
        if (token.isCancellationRequested) {
            return [];
        }
        if (sourceLink.href.kind === documentLinks_1.HrefKind.External) {
            const references = [];
            for (const link of allLinksInWorkspace) {
                if (link.href.kind === documentLinks_1.HrefKind.External && link.href.uri.toString() === sourceLink.href.uri.toString()) {
                    const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && (0, range_1.areRangesEqual)(sourceLink.source.hrefRange, link.source.hrefRange);
                    references.push({
                        kind: MdReferenceKind.Link,
                        isTriggerLocation,
                        isDefinition: false,
                        link,
                        location: { uri: link.source.resource.toString(), range: link.source.hrefRange },
                    });
                }
            }
            return references;
        }
        const resolvedResource = await (0, workspace_1.statLinkToMarkdownFile)(this.#configuration, this.#workspace, sourceLink.href.path);
        if (token.isCancellationRequested) {
            return [];
        }
        const references = [];
        if (resolvedResource && this.#isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.fragmentRange && (0, range_1.rangeContains)(sourceLink.source.fragmentRange, triggerPosition)) {
            const toc = await this.#tocProvider.get(resolvedResource);
            const entry = toc.lookup(sourceLink.href.fragment);
            if (entry) {
                references.push({
                    kind: MdReferenceKind.Header,
                    isTriggerLocation: false,
                    isDefinition: true,
                    location: entry.headerLocation,
                    headerText: entry.text,
                    headerTextLocation: entry.headerTextLocation
                });
            }
            for (const link of allLinksInWorkspace) {
                if (link.href.kind !== documentLinks_1.HrefKind.Internal || !(0, documentLinks_1.looksLikeLinkToResource)(this.#configuration, link.href, resolvedResource)) {
                    continue;
                }
                if (this.#parser.slugifier.fromHeading(link.href.fragment).equals(this.#parser.slugifier.fromHeading(sourceLink.href.fragment))) {
                    const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && (0, range_1.areRangesEqual)(sourceLink.source.hrefRange, link.source.hrefRange);
                    references.push({
                        kind: MdReferenceKind.Link,
                        isTriggerLocation,
                        isDefinition: false,
                        link,
                        location: { uri: link.source.resource.toString(), range: link.source.hrefRange },
                    });
                }
            }
        }
        else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
            references.push(...this.#findLinksToFile(resolvedResource ?? sourceLink.href.path, allLinksInWorkspace, sourceLink));
        }
        return references;
    }
    async #getAllLinksInWorkspace() {
        return (await this.#linkCache.values()).flat();
    }
    #isMarkdownPath(resolvedHrefPath) {
        return this.#workspace.hasMarkdownDocument(resolvedHrefPath) || (0, file_1.looksLikeMarkdownUri)(this.#configuration, resolvedHrefPath);
    }
    *#findLinksToFile(resource, links, sourceLink) {
        for (const link of links) {
            if (link.href.kind !== documentLinks_1.HrefKind.Internal || !(0, documentLinks_1.looksLikeLinkToResource)(this.#configuration, link.href, resource)) {
                continue;
            }
            // Exclude cases where the file is implicitly referencing itself
            if (link.source.hrefText.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
                continue;
            }
            const isTriggerLocation = !!sourceLink && sourceLink.source.resource.fsPath === link.source.resource.fsPath && (0, range_1.areRangesEqual)(sourceLink.source.hrefRange, link.source.hrefRange);
            const pathRange = this.#getPathRange(link);
            yield {
                kind: MdReferenceKind.Link,
                isTriggerLocation,
                isDefinition: false,
                link,
                location: { uri: link.source.resource.toString(), range: pathRange },
            };
        }
    }
    *#getReferencesToLinkReference(allLinks, refToFind, from) {
        for (const link of allLinks) {
            let ref;
            if (link.kind === documentLinks_1.MdLinkKind.Definition) {
                ref = link.ref.text;
            }
            else if (link.href.kind === documentLinks_1.HrefKind.Reference) {
                ref = link.href.ref;
            }
            else {
                continue;
            }
            if (ref === refToFind && link.source.resource.fsPath === from.resource.fsPath) {
                const isTriggerLocation = from.resource.fsPath === link.source.resource.fsPath && ((link.href.kind === documentLinks_1.HrefKind.Reference && (0, range_1.areRangesEqual)(from.range, link.source.hrefRange)) || (link.kind === documentLinks_1.MdLinkKind.Definition && (0, range_1.areRangesEqual)(from.range, link.ref.range)));
                const pathRange = this.#getPathRange(link);
                yield {
                    kind: MdReferenceKind.Link,
                    isTriggerLocation,
                    isDefinition: link.kind === documentLinks_1.MdLinkKind.Definition,
                    link,
                    location: { uri: from.resource.toString(), range: pathRange },
                };
            }
        }
    }
    /**
     * Get just the range of the file path, dropping the fragment
     */
    #getPathRange(link) {
        return link.source.fragmentRange
            ? (0, range_1.modifyRange)(link.source.hrefRange, undefined, (0, position_1.translatePosition)(link.source.fragmentRange.start, { characterDelta: -1 }))
            : link.source.hrefRange;
    }
}
exports.MdReferencesProvider = MdReferencesProvider;
//# sourceMappingURL=references.js.map