/******************************************************************************
 * Copyright 2021 TypeFox GmbH
 * This program and the accompanying materials are made available under the
 * terms of the MIT License, which is available in the project root.
 ******************************************************************************/
import * as ast from '../../languages/generated/ast.js';
import { isAstNode } from '../../syntax-tree.js';
import { getContainerOfType } from '../../utils/ast-utils.js';
import { getCrossReferenceTerminal, getExplicitRuleType, getTypeName, isArrayCardinality, isOptionalCardinality, terminalRegex } from '../../utils/grammar-utils.js';
/**
 * Calculates any features that can follow the given feature stack.
 * This also includes features following optional features and features from previously called rules that could follow the last feature.
 * @param featureStack A stack of features starting at the entry rule and ending at the feature of the current cursor position.
 * @param unparsedTokens All tokens which haven't been parsed successfully yet. This is the case when we call this function inside an alternative.
 * @returns Any `AbstractElement` that could be following the given feature stack.
 */
export function findNextFeatures(featureStack, unparsedTokens) {
    const context = {
        stacks: featureStack,
        tokens: unparsedTokens
    };
    interpretTokens(context);
    // Reset the container property
    context.stacks.flat().forEach(feature => { feature.property = undefined; });
    const nextStacks = findNextFeatureStacks(context.stacks);
    // We only need the last element of each stack
    return nextStacks.map(e => e[e.length - 1]);
}
function findNextFeaturesInternal(options) {
    const { next, cardinalities, visited, plus } = options;
    const features = [];
    const feature = next.feature;
    if (visited.has(feature)) {
        return [];
    }
    else if (!ast.isGroup(feature)) {
        // Do not add the feature to the list if it is a group
        // `findFirstFeaturesInternal` will take care of this
        visited.add(feature);
    }
    let parent;
    let item = feature;
    while (item.$container) {
        if (ast.isGroup(item.$container)) {
            parent = item.$container;
            break;
        }
        else if (ast.isAbstractElement(item.$container)) {
            item = item.$container;
        }
        else {
            break;
        }
    }
    // First try to iterate the same element again
    if (isArrayCardinality(item.cardinality)) {
        const repeatingFeatures = findFirstFeaturesInternal({
            next: {
                feature: item,
                type: next.type
            },
            cardinalities,
            visited,
            plus
        });
        for (const repeatingFeature of repeatingFeatures) {
            plus.add(repeatingFeature.feature);
        }
        features.push(...repeatingFeatures);
    }
    if (parent) {
        const ownIndex = parent.elements.indexOf(item);
        // Find next elements of the same group
        if (ownIndex !== undefined && ownIndex < parent.elements.length - 1) {
            features.push(...findNextFeaturesInGroup({
                feature: parent,
                type: next.type
            }, ownIndex + 1, cardinalities, visited, plus));
        }
        // Try to find the next elements of the parent
        // Only do this if every following element is either optional or has been parsed as +
        if (features.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)) || plus.has(e.feature))) {
            features.push(...findNextFeaturesInternal({
                next: {
                    feature: parent,
                    type: next.type
                },
                cardinalities,
                visited,
                plus
            }));
        }
    }
    return features;
}
/**
 * Calculates the first child feature of any `AbstractElement`.
 * @param next The `AbstractElement` whose first child features should be calculated.
 */
export function findFirstFeatures(next) {
    if (isAstNode(next)) {
        next = { feature: next };
    }
    return findFirstFeaturesInternal({ next, cardinalities: new Map(), visited: new Set(), plus: new Set() });
}
function findFirstFeaturesInternal(options) {
    var _a, _b, _c;
    const { next, cardinalities, visited, plus } = options;
    if (next === undefined) {
        return [];
    }
    const { feature, type } = next;
    if (ast.isGroup(feature)) {
        if (visited.has(feature)) {
            return [];
        }
        else {
            visited.add(feature);
        }
        return findNextFeaturesInGroup(next, 0, cardinalities, visited, plus)
            .map(e => modifyCardinality(e, feature.cardinality, cardinalities));
    }
    else if (ast.isAlternatives(feature) || ast.isUnorderedGroup(feature)) {
        return feature.elements.flatMap(e => findFirstFeaturesInternal({
            next: {
                feature: e,
                type,
                property: next.property
            },
            cardinalities,
            visited,
            plus
        }))
            .map(e => modifyCardinality(e, feature.cardinality, cardinalities));
    }
    else if (ast.isAssignment(feature)) {
        const assignmentNext = {
            feature: feature.terminal,
            type,
            property: (_a = next.property) !== null && _a !== void 0 ? _a : feature.feature
        };
        return findFirstFeaturesInternal({ next: assignmentNext, cardinalities, visited, plus })
            .map(e => modifyCardinality(e, feature.cardinality, cardinalities));
    }
    else if (ast.isAction(feature)) {
        return findNextFeaturesInternal({
            next: {
                feature,
                type: getTypeName(feature),
                property: (_b = next.property) !== null && _b !== void 0 ? _b : feature.feature
            },
            cardinalities,
            visited,
            plus
        });
    }
    else if (ast.isRuleCall(feature) && ast.isParserRule(feature.rule.ref)) {
        const rule = feature.rule.ref;
        const ruleCallNext = {
            feature: rule.definition,
            type: rule.fragment || rule.dataType ? undefined : ((_c = getExplicitRuleType(rule)) !== null && _c !== void 0 ? _c : rule.name),
            property: next.property
        };
        return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
            .map(e => modifyCardinality(e, feature.cardinality, cardinalities));
    }
    else {
        return [next];
    }
}
/**
 * Modifying the cardinality is necessary to identify which features are coming from an optional feature.
 * Those features should be optional as well.
 * @param next The next feature that could be made optionally.
 * @param cardinality The cardinality of the calling (parent) object.
 * @returns A new feature that could be now optional (`?` or `*`).
 */
function modifyCardinality(next, cardinality, cardinalities) {
    cardinalities.set(next.feature, cardinality);
    return next;
}
function findNextFeaturesInGroup(next, index, cardinalities, visited, plus) {
    var _a;
    const features = [];
    let firstFeature;
    while (index < next.feature.elements.length) {
        const feature = next.feature.elements[index++];
        firstFeature = {
            feature,
            type: next.type
        };
        features.push(...findFirstFeaturesInternal({
            next: firstFeature,
            cardinalities,
            visited,
            plus
        }));
        if (!isOptionalCardinality((_a = firstFeature.feature.cardinality) !== null && _a !== void 0 ? _a : cardinalities.get(firstFeature.feature), firstFeature.feature)) {
            break;
        }
    }
    return features;
}
function interpretTokens(context) {
    for (const token of context.tokens) {
        const nextFeatureStacks = findNextFeatureStacks(context.stacks, token);
        context.stacks = nextFeatureStacks;
    }
}
function findNextFeatureStacks(stacks, token) {
    const newStacks = [];
    for (const stack of stacks) {
        newStacks.push(...interpretStackToken(stack, token));
    }
    return newStacks;
}
function interpretStackToken(stack, token) {
    const cardinalities = new Map();
    const plus = new Set(stack.map(e => e.feature).filter(isPlusFeature));
    const newStacks = [];
    while (stack.length > 0) {
        const top = stack.pop();
        const allNextFeatures = findNextFeaturesInternal({
            next: top,
            cardinalities,
            plus,
            visited: new Set()
        }).filter(next => token ? featureMatches(next.feature, token) : true);
        for (const nextFeature of allNextFeatures) {
            newStacks.push([...stack, nextFeature]);
        }
        if (!allNextFeatures.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)))) {
            break;
        }
    }
    return newStacks;
}
function isPlusFeature(feature) {
    if (feature.cardinality === '+') {
        return true;
    }
    const assignment = getContainerOfType(feature, ast.isAssignment);
    if (assignment && assignment.cardinality === '+') {
        return true;
    }
    return false;
}
function featureMatches(feature, token) {
    if (ast.isKeyword(feature)) {
        const content = feature.value;
        return content === token.image;
    }
    else if (ast.isRuleCall(feature)) {
        return ruleMatches(feature.rule.ref, token);
    }
    else if (ast.isCrossReference(feature)) {
        const crossRefTerminal = getCrossReferenceTerminal(feature);
        if (crossRefTerminal) {
            return featureMatches(crossRefTerminal, token);
        }
    }
    return false;
}
function ruleMatches(rule, token) {
    if (ast.isParserRule(rule)) {
        const ruleFeatures = findFirstFeatures(rule.definition);
        return ruleFeatures.some(e => featureMatches(e.feature, token));
    }
    else if (ast.isTerminalRule(rule)) {
        // We have to take keywords into account
        // e.g. most keywords are valid IDs as well
        // Only return 'true' if this terminal does not match a keyword. TODO
        return terminalRegex(rule).test(token.image);
    }
    else {
        return false;
    }
}//# sourceMappingURL=https://cdn.windsurf.com/sourcemaps/f90ca8311d630b9b77e7537222ecfce0a0ac990b/node_modules/langium/lib/lsp/completion/follow-element-computation.js.map