"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MdSelectionRangeProvider = 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 arrays_1 = require("../util/arrays");
const string_1 = require("../util/string");
class MdSelectionRangeProvider {
    #parser;
    #tocProvider;
    #logger;
    constructor(parser, tocProvider, logger) {
        this.#parser = parser;
        this.#tocProvider = tocProvider;
        this.#logger = logger;
    }
    async provideSelectionRanges(document, positions, token) {
        this.#logger.log(logging_1.LogLevel.Debug, 'MdSelectionRangeProvider.provideSelectionRanges', { document: document.uri, version: document.version });
        return (0, arrays_1.coalesce)(await Promise.all(positions.map(position => this.#provideSelectionRange(document, position, token))));
    }
    async #provideSelectionRange(document, position, token) {
        const headerRange = await this.#getHeaderSelectionRange(document, position, token);
        if (token.isCancellationRequested) {
            return;
        }
        const blockRange = await this.#getBlockSelectionRange(document, position, headerRange, token);
        if (token.isCancellationRequested) {
            return;
        }
        const inlineRange = createInlineRange(document, position, blockRange);
        return inlineRange ?? blockRange ?? headerRange;
    }
    async #getBlockSelectionRange(document, position, parent, token) {
        const tokens = await this.#parser.tokenize(document);
        if (token.isCancellationRequested) {
            return undefined;
        }
        const blockTokens = getBlockTokensForPosition(tokens, position, parent);
        if (blockTokens.length === 0) {
            return undefined;
        }
        let currentRange = parent ?? createBlockRange(blockTokens.shift(), document, position.line, undefined);
        for (let i = 0; i < blockTokens.length; i++) {
            currentRange = createBlockRange(blockTokens[i], document, position.line, currentRange);
        }
        return currentRange;
    }
    async #getHeaderSelectionRange(document, position, token) {
        const toc = await this.#tocProvider.getForDocument(document);
        if (token.isCancellationRequested) {
            return undefined;
        }
        const headerInfo = getHeadersForPosition(toc.entries, position);
        const headers = headerInfo.headers;
        let currentRange;
        for (let i = 0; i < headers.length; i++) {
            currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc.entries));
        }
        return currentRange;
    }
}
exports.MdSelectionRangeProvider = MdSelectionRangeProvider;
function getHeadersForPosition(toc, position) {
    const enclosingHeaders = toc.filter(header => header.sectionLocation.range.start.line <= position.line && header.sectionLocation.range.end.line >= position.line);
    const sortedHeaders = enclosingHeaders.sort((header1, header2) => (header1.line - position.line) - (header2.line - position.line));
    const onThisLine = toc.find(header => header.line === position.line) !== undefined;
    return {
        headers: sortedHeaders,
        headerOnThisLine: onThisLine
    };
}
function createHeaderRange(header, isClosestHeaderToPosition, onHeaderLine, parent, startOfChildRange) {
    const range = header.sectionLocation.range;
    const contentRange = (0, range_1.makeRange)((0, position_1.translatePosition)(range.start, { lineDelta: 1 }), range.end);
    if (onHeaderLine && isClosestHeaderToPosition && startOfChildRange) {
        // selection was made on this header line, so select header and its content until the start of its first child
        // then all of its content
        return makeSelectionRange((0, range_1.modifyRange)(range, undefined, startOfChildRange), makeSelectionRange(range, parent));
    }
    else if (onHeaderLine && isClosestHeaderToPosition) {
        // selection was made on this header line and no children so expand to all of its content
        return makeSelectionRange(range, parent);
    }
    else if (isClosestHeaderToPosition && startOfChildRange) {
        // selection was made within content and has child so select content
        // of this header then all content then header
        return makeSelectionRange((0, range_1.modifyRange)(contentRange, undefined, startOfChildRange), makeSelectionRange(contentRange, (makeSelectionRange(range, parent))));
    }
    else {
        // not on this header line so select content then header
        return makeSelectionRange(contentRange, makeSelectionRange(range, parent));
    }
}
function getBlockTokensForPosition(tokens, position, parent) {
    const enclosingTokens = tokens.filter((token) => !!token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token));
    if (enclosingTokens.length === 0) {
        return [];
    }
    const sortedTokens = enclosingTokens.sort((token1, token2) => (token2.map[1] - token2.map[0]) - (token1.map[1] - token1.map[0]));
    return sortedTokens;
}
function createBlockRange(block, document, cursorLine, parent) {
    if (block.type === 'fence') {
        return createFencedRange(block, cursorLine, document, parent);
    }
    let startLine = (0, string_1.isEmptyOrWhitespace)((0, textDocument_1.getLine)(document, block.map[0])) ? block.map[0] + 1 : block.map[0];
    let endLine = startLine === block.map[1] ? block.map[1] : block.map[1] - 1;
    if (block.type === 'paragraph_open' && block.map[1] - block.map[0] === 2) {
        startLine = endLine = cursorLine;
    }
    else if (isList(block) && (0, string_1.isEmptyOrWhitespace)((0, textDocument_1.getLine)(document, endLine))) {
        endLine = endLine - 1;
    }
    const range = (0, range_1.makeRange)(startLine, 0, endLine, (0, textDocument_1.getLine)(document, endLine).length);
    if (parent && (0, range_1.rangeContains)(parent.range, range) && !(0, range_1.areRangesEqual)(parent.range, range)) {
        return makeSelectionRange(range, parent);
    }
    else if (parent && (0, range_1.areRangesEqual)(parent.range, range)) {
        return parent;
    }
    else {
        return makeSelectionRange(range, undefined);
    }
}
function createInlineRange(document, cursorPosition, parent) {
    const lineText = (0, textDocument_1.getLine)(document, cursorPosition.line);
    const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent);
    const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent);
    let comboSelection;
    if (boldSelection && italicSelection && !(0, range_1.areRangesEqual)(boldSelection.range, italicSelection.range)) {
        if ((0, range_1.rangeContains)(boldSelection.range, italicSelection.range)) {
            comboSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, boldSelection);
        }
        else if ((0, range_1.rangeContains)(italicSelection.range, boldSelection.range)) {
            comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection);
        }
    }
    const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection ?? boldSelection ?? italicSelection ?? parent);
    const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection ?? parent);
    return inlineCodeBlockSelection ?? linkSelection ?? comboSelection ?? boldSelection ?? italicSelection;
}
function createFencedRange(token, cursorLine, document, parent) {
    const startLine = token.map[0];
    const endLine = token.map[1] - 1;
    const onFenceLine = cursorLine === startLine || cursorLine === endLine;
    const fenceRange = (0, range_1.makeRange)(startLine, 0, endLine, (0, textDocument_1.getLine)(document, endLine).length);
    const contentRange = endLine - startLine > 2 && !onFenceLine ? (0, range_1.makeRange)(startLine + 1, 0, endLine - 1, (0, textDocument_1.getLine)(document, endLine - 1).length) : undefined;
    if (contentRange) {
        return makeSelectionRange(contentRange, makeSelectionRange(fenceRange, parent));
    }
    else {
        if (parent && (0, range_1.areRangesEqual)(parent.range, fenceRange)) {
            return parent;
        }
        else {
            return makeSelectionRange(fenceRange, parent);
        }
    }
}
function createBoldRange(lineText, cursorChar, cursorLine, parent) {
    const regex = /\*\*([^*]+\*?[^*]+\*?[^*]+)\*\*/gim;
    const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
    if (matches.length) {
        // should only be one match, so select first and index 0 contains the entire match
        const bold = matches[0][0];
        const startIndex = lineText.indexOf(bold);
        const cursorOnStars = cursorChar === startIndex || cursorChar === startIndex + 1 || cursorChar === startIndex + bold.length || cursorChar === startIndex + bold.length - 1;
        const contentAndStars = makeSelectionRange((0, range_1.makeRange)(cursorLine, startIndex, cursorLine, startIndex + bold.length), parent);
        const content = makeSelectionRange((0, range_1.makeRange)(cursorLine, startIndex + 2, cursorLine, startIndex + bold.length - 2), contentAndStars);
        return cursorOnStars ? contentAndStars : content;
    }
    return undefined;
}
function createOtherInlineRange(lineText, cursorChar, cursorLine, isItalic, parent) {
    const italicRegexes = [/(?:[^*]+)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]+)/g, /^(?:[^*]*)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]*)$/g];
    let matches = [];
    if (isItalic) {
        matches = [...lineText.matchAll(italicRegexes[0])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
        if (!matches.length) {
            matches = [...lineText.matchAll(italicRegexes[1])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
        }
    }
    else {
        matches = [...lineText.matchAll(/\`[^\`]*\`/g)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
    }
    if (matches.length) {
        // should only be one match, so select first and select group 1 for italics because that contains just the italic section
        // doesn't include the leading and trailing characters which are guaranteed to not be * so as not to be confused with bold
        const match = isItalic ? matches[0][1] : matches[0][0];
        const startIndex = lineText.indexOf(match);
        const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length;
        const contentAndType = makeSelectionRange((0, range_1.makeRange)(cursorLine, startIndex, cursorLine, startIndex + match.length), parent);
        const content = makeSelectionRange((0, range_1.makeRange)(cursorLine, startIndex + 1, cursorLine, startIndex + match.length - 1), contentAndType);
        return cursorOnType ? contentAndType : content;
    }
    return undefined;
}
function createLinkRange(lineText, cursorChar, cursorLine, parent) {
    const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g;
    const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar);
    if (matches.length) {
        // should only be one match, so select first and index 0 contains the entire match, so match = [text](url)
        const link = matches[0][0];
        const linkRange = makeSelectionRange((0, range_1.makeRange)(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent);
        const linkText = matches[0][1];
        const url = matches[0][2];
        // determine if cursor is within [text] or (url) in order to know which should be selected
        const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url;
        const indexOfType = lineText.indexOf(nearestType);
        // determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range
        const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length;
        const contentAndNearestType = makeSelectionRange((0, range_1.makeRange)(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange);
        const content = makeSelectionRange((0, range_1.makeRange)(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType);
        return cursorOnType ? contentAndNearestType : content;
    }
    return undefined;
}
function isList(token) {
    return token.type ? ['ordered_list_open', 'list_item_open', 'bullet_list_open'].includes(token.type) : false;
}
function isBlockElement(token) {
    return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type);
}
function getFirstChildHeader(document, header, toc) {
    let childRange;
    if (header && toc) {
        const children = toc.filter(t => (0, range_1.rangeContains)(header.sectionLocation.range, t.sectionLocation.range) && t.sectionLocation.range.start.line > header.sectionLocation.range.start.line).sort((t1, t2) => t1.line - t2.line);
        if (children.length > 0) {
            childRange = children[0].sectionLocation.range.start;
            const lineText = (0, textDocument_1.getLine)(document, childRange.line - 1);
            return childRange ? (0, position_1.translatePosition)(childRange, { lineDelta: -1, characterDelta: lineText.length }) : undefined;
        }
    }
    return undefined;
}
function makeSelectionRange(range, parent) {
    return { range, parent };
}
//# sourceMappingURL=smartSelect.js.map