import { LogLevel } from '../logging.js';
import { HrefKind, MdLinkKind } from '../types/documentLink.js';
import { translatePosition } from '../types/position.js';
import { areRangesEqual, modifyRange, rangeContains } from '../types/range.js';
import { getDocUri } from '../types/textDocument.js';
import { Disposable } from '../util/dispose.js';
import { isSameResource, looksLikeMarkdownUri, looksLikePathToResource } from '../util/path.js';
import { statLinkToMarkdownFile } from '../workspace.js';
export var MdReferenceKind;
(function (MdReferenceKind) {
    MdReferenceKind[MdReferenceKind["Link"] = 1] = "Link";
    MdReferenceKind[MdReferenceKind["Header"] = 2] = "Header";
    MdReferenceKind[MdReferenceKind["HtmlId"] = 3] = "HtmlId";
})(MdReferenceKind || (MdReferenceKind = {}));
/**
 * Stateful object that computes references for markdown files.
 */
export class MdReferencesProvider extends 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(LogLevel.Debug, 'ReferencesProvider.getReferencesAtPosition', { document: document.uri, version: document.version });
        const toc = await this.#tocProvider.getForDocument(document, { includeHtmlIds: true });
        if (token.isCancellationRequested) {
            return [];
        }
        const header = toc.entries.find(entry => rangeContains(entry.declarationLocation.range, position));
        if (header) {
            if (header.kind === 'header') {
                return this.#getReferencesToHeader(document, header, token);
            }
            else if (header.kind === 'html-id') {
                return this.#getReferencesToHtmlId(document, header, token);
            }
            else {
                return [];
            }
        }
        else {
            return this.#getReferencesToLinkAtPosition(document, position, token);
        }
    }
    async getReferencesToFileInWorkspace(resource, token) {
        this.#logger.log(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.declarationLocation,
            header,
        });
        for (const link of links) {
            if (link.href.kind === HrefKind.Internal
                && looksLikePathToResource(this.#configuration, link.href.path, getDocUri(document))
                && this.#parser.slugifier.fromFragment(link.href.fragment).equals(header.slug)) {
                references.push({
                    kind: MdReferenceKind.Link,
                    isTriggerLocation: false,
                    isDefinition: false,
                    link,
                    location: { uri: link.source.resource.toString(), range: link.source.hrefRange },
                });
            }
        }
        return references;
    }
    async #getReferencesToHtmlId(document, htmlId, token) {
        const links = await this.#getAllLinksInWorkspace();
        if (token.isCancellationRequested) {
            return [];
        }
        const references = [];
        references.push({
            kind: MdReferenceKind.HtmlId,
            isTriggerLocation: true,
            isDefinition: true,
            location: htmlId.declarationLocation,
            htmlId,
        });
        for (const link of links) {
            if (link.href.kind === HrefKind.Internal
                && looksLikePathToResource(this.#configuration, link.href.path, getDocUri(document))
                && this.#parser.slugifier.fromFragment(link.href.fragment).equals(htmlId.slug)) {
                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 === MdLinkKind.Definition) {
                // We could be in either the ref name or the definition
                if (rangeContains(link.ref.range, position)) {
                    return Array.from(this.#getReferencesToLinkReference(docLinks, link.ref.text, { resource: getDocUri(document), range: link.ref.range }));
                }
                else if (rangeContains(link.source.hrefRange, position)) {
                    return this.#getReferencesToLink(docLinks, link, position, token);
                }
            }
            else {
                if (rangeContains(link.source.hrefRange, position)) {
                    return this.#getReferencesToLink(docLinks, link, position, token);
                }
            }
        }
        return [];
    }
    async #getReferencesToLink(docLinks, sourceLink, triggerPosition, token) {
        if (sourceLink.href.kind === 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 === HrefKind.External) {
            const references = [];
            for (const link of allLinksInWorkspace) {
                if (link.href.kind === HrefKind.External && isSameResource(link.href.uri, sourceLink.href.uri)) {
                    const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && 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 statLinkToMarkdownFile(this.#configuration, this.#workspace, sourceLink.href.path);
        if (token.isCancellationRequested) {
            return [];
        }
        const references = [];
        if (resolvedResource && this.#isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.hrefFragmentRange && rangeContains(sourceLink.source.hrefFragmentRange, triggerPosition)) {
            const toc = await this.#tocProvider.get(resolvedResource, { includeHtmlIds: true });
            const entry = toc?.lookupByFragment(sourceLink.href.fragment);
            if (entry && entry.kind === 'header') {
                references.push({
                    kind: MdReferenceKind.Header,
                    isTriggerLocation: false,
                    isDefinition: true,
                    location: entry.declarationLocation,
                    header: entry,
                });
            }
            else if (entry && entry.kind === 'html-id') {
                references.push({
                    kind: MdReferenceKind.HtmlId,
                    isTriggerLocation: false,
                    isDefinition: true,
                    location: entry.declarationLocation,
                    htmlId: entry,
                });
            }
            for (const link of allLinksInWorkspace) {
                if (link.href.kind !== HrefKind.Internal || !looksLikePathToResource(this.#configuration, link.href.path, 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 && 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) || looksLikeMarkdownUri(this.#configuration, resolvedHrefPath);
    }
    *#findLinksToFile(resource, links, sourceLink) {
        for (const link of links) {
            if (link.href.kind !== HrefKind.Internal || !looksLikePathToResource(this.#configuration, link.href.path, 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 && 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 === MdLinkKind.Definition) {
                ref = link.ref.text;
            }
            else if (link.href.kind === 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 === HrefKind.Reference && areRangesEqual(from.range, link.source.hrefRange)) || (link.kind === MdLinkKind.Definition && areRangesEqual(from.range, link.ref.range)));
                const pathRange = this.#getPathRange(link);
                yield {
                    kind: MdReferenceKind.Link,
                    isTriggerLocation,
                    isDefinition: link.kind === 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.hrefFragmentRange
            ? modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 }))
            : link.source.hrefRange;
    }
}
//# sourceMappingURL=references.js.map