import { CompletionItemKind, } from 'vscode-languageserver-types';
import { isLabelLeaf } from '../types';
import { findParent } from '../helpers';
import { NodePatternContext, PatternElementContext, QuantifierContext, RelationshipPatternContext, } from '../generated-parser/CypherCmdParser';
import { backtickIfNeeded } from './autocompletionHelpers';
export const labelsToCompletions = (labelNames = []) => labelNames.map((labelName) => {
    const backtickedName = backtickIfNeeded(labelName, 'label');
    const maybeInsertText = backtickedName
        ? { insertText: backtickedName }
        : {};
    const result = {
        label: labelName,
        kind: CompletionItemKind.TypeParameter,
        ...maybeInsertText,
    };
    return result;
});
export const allLabelCompletions = (dbSchema) => labelsToCompletions(dbSchema.labels);
export const reltypesToCompletions = (reltypes = []) => reltypes.map((relType) => {
    const backtickedName = backtickIfNeeded(relType, 'relType');
    const maybeInsertText = backtickedName
        ? { insertText: backtickedName }
        : {};
    const result = {
        label: relType,
        kind: CompletionItemKind.TypeParameter,
        ...maybeInsertText,
    };
    return result;
});
export const allReltypeCompletions = (dbSchema) => reltypesToCompletions(dbSchema.relationshipTypes);
function intersectChildren(incomingLabels, outGoingLabels, children) {
    let inLabels = undefined;
    let outLabels = undefined;
    children.forEach((c) => {
        const { inLabels: incoming, outLabels: outgoing } = walkLabelTree(incomingLabels, outGoingLabels, c);
        if (!inLabels) {
            inLabels = incoming;
            outLabels = outgoing;
        }
        else {
            inLabels = inLabels.intersection(incoming);
            outLabels = outLabels.intersection(outgoing);
        }
    });
    if (!inLabels) {
        inLabels = new Set();
        outLabels = new Set();
    }
    return { inLabels, outLabels };
}
function uniteChildren(incomingLabels, outGoingLabels, children) {
    let inLabels = new Set();
    let outLabels = new Set();
    children.forEach((c) => {
        const { inLabels: incoming, outLabels: outgoing } = walkLabelTree(incomingLabels, outGoingLabels, c);
        inLabels = inLabels.union(incoming);
        outLabels = outLabels.union(outgoing);
    });
    return { inLabels, outLabels };
}
function walkLabelTree(incomingLabels, outGoingLabels, labelTree) {
    if (isLabelLeaf(labelTree)) {
        const incoming = incomingLabels.get(labelTree.value);
        const outgoing = outGoingLabels.get(labelTree.value);
        return {
            inLabels: incoming ?? new Set(),
            outLabels: outgoing ?? new Set(),
        };
    }
    else if (labelTree.andOr == 'and') {
        return intersectChildren(incomingLabels, outGoingLabels, labelTree.children);
    }
    else {
        return uniteChildren(incomingLabels, outGoingLabels, labelTree.children);
    }
}
function getRelsFromNodesSets(dbSchema) {
    if (dbSchema.graphSchema) {
        const toNodes = new Map();
        const fromNodes = new Map();
        dbSchema.graphSchema.forEach((rel) => {
            //rels in schema defined like (from)-(relType)->(to)
            //Means 'from' is "node going to rel", hence why we
            //pass rel.from into toNodes
            if (!toNodes.has(rel.from)) {
                toNodes.set(rel.from, new Set());
            }
            if (!fromNodes.has(rel.to)) {
                fromNodes.set(rel.to, new Set());
            }
            toNodes.get(rel.from).add(rel.relType);
            fromNodes.get(rel.to).add(rel.relType);
        });
        return { toNodes, fromNodes };
    }
    return undefined;
}
function getNodesFromRelsSet(dbSchema) {
    if (dbSchema.graphSchema) {
        const toRels = new Map();
        const fromRels = new Map();
        dbSchema.graphSchema.forEach((rel) => {
            if (!toRels.has(rel.relType)) {
                toRels.set(rel.relType, new Set());
                fromRels.set(rel.relType, new Set());
            }
            toRels.get(rel.relType).add(rel.to);
            fromRels.get(rel.relType).add(rel.from);
        });
        return { toRels, fromRels };
    }
    return undefined;
}
function findLastVariable(lastValidElement, symbolsInfo) {
    const variable = lastValidElement.variable();
    const isAnonVar = variable === null;
    const foundVariable = isAnonVar
        ? symbolsInfo?.symbolTables?.flat().find((entry) => 
        // Because the anonymous variable created in the AST is "made up", it doesnt have a position of its own in the query.
        // It therefor inherits the parent-nodes (The NodePattern/RelationshipPattern = lastValidElement) position
        entry.definitionPosition >= lastValidElement.start.start &&
            entry.definitionPosition <= lastValidElement.stop.stop)
        : symbolsInfo?.symbolTables
            ?.flat()
            .find((entry) => entry.references.includes(variable.start.start));
    return foundVariable;
}
export function completeNodeLabel(dbSchema, parsingResult, symbolsInfo) {
    if (dbSchema.graphSchema === undefined) {
        return allLabelCompletions(dbSchema);
    }
    const callContext = findParent(parsingResult.stopNode.parentCtx, (x) => x instanceof PatternElementContext);
    if (callContext instanceof PatternElementContext) {
        const lastValidElement = callContext.children.toReversed().find((child) => {
            if (child instanceof RelationshipPatternContext) {
                if (child.exception === null) {
                    return true;
                }
            }
        });
        // limitation: bailing out on quantifiers
        if (lastValidElement instanceof QuantifierContext) {
            return allLabelCompletions(dbSchema);
        }
        if (lastValidElement instanceof RelationshipPatternContext) {
            const foundVariable = findLastVariable(lastValidElement, symbolsInfo);
            if (foundVariable === undefined ||
                ('children' in foundVariable.labels &&
                    foundVariable.labels.children.length == 0)) {
                return allLabelCompletions(dbSchema);
            }
            const direction = lastValidElement.leftArrow()
                ? 'outgoing'
                : lastValidElement.rightArrow()
                    ? 'incoming'
                    : 'bidirectional';
            // limitation: not direction-aware (ignores <- vs ->)
            // limitation: not checking node label repetition
            const { toRels: nodesToRelsSet, fromRels: nodesFromRelsSet } = getNodesFromRelsSet(dbSchema);
            const { inLabels, outLabels } = walkLabelTree(nodesToRelsSet, nodesFromRelsSet, foundVariable.labels);
            const allNodes = direction === 'outgoing'
                ? outLabels
                : direction === 'incoming'
                    ? inLabels
                    : inLabels.union(outLabels);
            return labelsToCompletions(Array.from(allNodes));
        }
    }
    return allLabelCompletions(dbSchema);
}
export function completeRelationshipType(dbSchema, parsingResult, symbolsInfo) {
    if (dbSchema.graphSchema === undefined) {
        return allReltypeCompletions(dbSchema);
    }
    // limitation: not checking PathPatternNonEmptyContext
    // limitation: not handling parenthesized paths
    const patternContext = findParent(parsingResult.stopNode.parentCtx, (x) => x instanceof PatternElementContext);
    if (patternContext instanceof PatternElementContext) {
        const lastValidElement = patternContext.children
            .toReversed()
            .find((child) => {
            if (child instanceof NodePatternContext) {
                if (child.exception === null) {
                    return true;
                }
            }
        });
        const thisCtx = findParent(parsingResult.stopNode, (x) => x instanceof RelationshipPatternContext);
        let direction = 'bidirectional';
        if (thisCtx instanceof RelationshipPatternContext) {
            direction = thisCtx.leftArrow()
                ? 'outgoing'
                : thisCtx.rightArrow()
                    ? 'incoming'
                    : 'bidirectional';
        }
        // limitation: bailing out on quantifiers
        if (lastValidElement instanceof QuantifierContext) {
            return allReltypeCompletions(dbSchema);
        }
        if (lastValidElement instanceof NodePatternContext) {
            const foundVariable = findLastVariable(lastValidElement, symbolsInfo);
            if (foundVariable === undefined ||
                ('children' in foundVariable.labels &&
                    foundVariable.labels.children.length == 0)) {
                return allReltypeCompletions(dbSchema);
            }
            // limitation: not checking relationship type repetition
            const { toNodes: relsToNodesSet, fromNodes: relsFromNodesSet } = getRelsFromNodesSets(dbSchema);
            const { inLabels, outLabels } = walkLabelTree(relsToNodesSet, relsFromNodesSet, foundVariable.labels);
            const allRels = direction === 'outgoing'
                ? outLabels
                : direction === 'incoming'
                    ? inLabels
                    : inLabels.union(outLabels);
            return reltypesToCompletions(Array.from(allRels));
        }
    }
    return allReltypeCompletions(dbSchema);
}
//# sourceMappingURL=schemaBasedCompletions.js.map