"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveSuffix = exports.doCompletionResolve = exports.doCompletion = void 0;
const os_1 = require("os");
const vscode_languageserver_1 = require("vscode-languageserver");
const yaml_1 = require("yaml");
const ansible_1 = require("../utils/ansible");
const docsFormatter_1 = require("../utils/docsFormatter");
const misc_1 = require("../utils/misc");
const yaml_2 = require("../utils/yaml");
const completionProviderUtils_1 = require("./completionProviderUtils");
const priorityMap = {
    nameKeyword: 1,
    moduleName: 2,
    redirectedModuleName: 3,
    keyword: 4,
    // options
    requiredOption: 1,
    option: 2,
    aliasOption: 3,
    // choices
    defaultChoice: 1,
    choice: 2,
};
let dummyMappingCharacter;
let isAnsiblePlaybook;
function doCompletion(document, position, context) {
    return __awaiter(this, void 0, void 0, function* () {
        isAnsiblePlaybook = (0, yaml_2.isPlaybook)(document);
        let preparedText = document.getText();
        const offset = document.offsetAt(position);
        // HACK: We need to insert a dummy mapping, so that the YAML parser can properly recognize the scope.
        // This is particularly important when parser has nothing more than indentation to
        // determine the scope of the current line.
        // This is handled w.r.t two scenarios:
        // 1. When we are at the key level, we use `_:` since we expect to work on a pair level.
        // 2. When we are at the value level, we use `__`. We do this because based on the above hack, the
        // use of `_:` at the value level creates invalid YAML as `: ` is an incorrect token in yaml string scalar
        dummyMappingCharacter = "_:";
        const previousCharactersOfCurrentLine = document.getText({
            start: { line: position.line, character: 0 },
            end: { line: position.line, character: position.character },
        });
        if (previousCharactersOfCurrentLine.includes(": ")) {
            // this means we have encountered ": " previously in the same line and thus we are
            // at the value level
            dummyMappingCharacter = "__";
        }
        preparedText = (0, misc_1.insert)(preparedText, offset, dummyMappingCharacter);
        const yamlDocs = (0, yaml_2.parseAllDocuments)(preparedText);
        const extensionSettings = yield context.documentSettings.get(document.uri);
        const useFqcn = extensionSettings.ansible.useFullyQualifiedCollectionNames;
        const provideRedirectModulesCompletion = extensionSettings.completion.provideRedirectModules;
        const provideModuleOptionAliasesCompletion = extensionSettings.completion.provideModuleOptionAliases;
        // We need inclusive matching, since cursor position is the position of the character right after it
        // NOTE: Might no longer be required due to the hack above
        const path = (0, yaml_2.getPathAt)(document, position, yamlDocs, true);
        if (path) {
            const node = path[path.length - 1];
            if (node) {
                const docsLibrary = yield context.docsLibrary;
                const isPlay = (0, yaml_2.isPlayParam)(path);
                if (isPlay) {
                    return getKeywordCompletion(document, position, path, ansible_1.playKeywords);
                }
                if ((0, yaml_2.isBlockParam)(path)) {
                    return getKeywordCompletion(document, position, path, ansible_1.blockKeywords);
                }
                if ((0, yaml_2.isRoleParam)(path)) {
                    return getKeywordCompletion(document, position, path, ansible_1.roleKeywords);
                }
                if ((0, yaml_2.isTaskParam)(path)) {
                    // offer basic task keywords
                    const completionItems = getKeywordCompletion(document, position, path, ansible_1.taskKeywords);
                    if (isPlay === undefined) {
                        // this can still turn into a play, so we should offer those keywords too
                        completionItems.push(...getKeywordCompletion(document, position, path, ansible_1.playWithoutTaskKeywords));
                    }
                    // incidentally, the hack mentioned above prevents finding a module in
                    // case the cursor is on it
                    const module = yield (0, yaml_2.findProvidedModule)(path, document, docsLibrary);
                    if (!module) {
                        // offer the 'block' keyword (as it is not one of taskKeywords)
                        completionItems.push(...getKeywordCompletion(document, position, path, new Map([["block", ansible_1.blockKeywords.get("block")]])));
                        const inlineCollections = (0, yaml_2.getDeclaredCollections)(path);
                        const cursorAtEndOfLine = atEndOfLine(document, position);
                        let textEdit;
                        const nodeRange = getNodeRange(node, document);
                        if (nodeRange) {
                            textEdit = {
                                range: nodeRange,
                                newText: "", // placeholder
                            };
                        }
                        const cursorAtFirstElementOfList = firstElementOfList(document, nodeRange);
                        // offer modules
                        const moduleCompletionItems = [
                            ...(yield docsLibrary.getModuleFqcns(document.uri)),
                        ]
                            .filter((moduleFqcn) => {
                            var _a;
                            return provideRedirectModulesCompletion ||
                                !((_a = docsLibrary.getModuleRoute(moduleFqcn)) === null || _a === void 0 ? void 0 : _a.redirect);
                        })
                            .map((moduleFqcn) => {
                            var _a;
                            let priority, kind;
                            if ((_a = docsLibrary.getModuleRoute(moduleFqcn)) === null || _a === void 0 ? void 0 : _a.redirect) {
                                priority = priorityMap.redirectedModuleName;
                                kind = vscode_languageserver_1.CompletionItemKind.Reference;
                            }
                            else {
                                priority = priorityMap.moduleName;
                                kind = vscode_languageserver_1.CompletionItemKind.Class;
                            }
                            const [namespace, collection, name] = moduleFqcn.split(".");
                            const insertName = useFqcn ? moduleFqcn : name;
                            const insertText = cursorAtEndOfLine
                                ? `${insertName}:${resolveSuffix("dict", // since a module is always a dictionary
                                cursorAtFirstElementOfList, isAnsiblePlaybook)}`
                                : insertName;
                            return {
                                label: useFqcn ? moduleFqcn : name,
                                kind: kind,
                                detail: `${namespace}.${collection}`,
                                sortText: useFqcn
                                    ? `${priority}_${moduleFqcn}`
                                    : `${priority}_${name}`,
                                filterText: useFqcn
                                    ? `${name} ${moduleFqcn} ${collection} ${namespace}` // name should have highest priority (in case of FQCN)
                                    : `${name} ${moduleFqcn}`,
                                data: {
                                    documentUri: document.uri,
                                    moduleFqcn: moduleFqcn,
                                    inlineCollections: inlineCollections,
                                    atEndOfLine: cursorAtEndOfLine,
                                    firstElementOfList: cursorAtFirstElementOfList,
                                },
                                textEdit: Object.assign(Object.assign({}, textEdit), { newText: insertText }),
                            };
                        });
                        completionItems.push(...moduleCompletionItems);
                    }
                    return completionItems;
                }
                // Provide variable auto-completion if the cursor is inside valid jinja inline brackets in a playbook
                if (isAnsiblePlaybook &&
                    (0, yaml_2.isCursorInsideJinjaBrackets)(document, position, path)) {
                    const varCompletion = (0, completionProviderUtils_1.getVarsCompletion)(document.uri, path);
                    return varCompletion;
                }
                // Check if we're looking for module options or sub-options
                const options = yield (0, yaml_2.getPossibleOptionsForPath)(path, document, docsLibrary);
                if (options) {
                    const optionMap = new yaml_2.AncestryBuilder(path)
                        .parentOfKey()
                        .get();
                    // find options that have been already provided by the user
                    const providedOptions = new Set((0, yaml_2.getYamlMapKeys)(optionMap));
                    const remainingOptions = [...options.entries()].filter(([, specs]) => !providedOptions.has(specs.name));
                    const nodeRange = getNodeRange(node, document);
                    const cursorAtFirstElementOfList = firstElementOfList(document, nodeRange);
                    const cursorAtEndOfLine = atEndOfLine(document, position);
                    return remainingOptions
                        .map(([option, specs]) => {
                        return {
                            name: option,
                            specs: specs,
                        };
                    })
                        .filter((option) => provideModuleOptionAliasesCompletion || !isAlias(option))
                        .map((option, index) => {
                        // translate option documentation to CompletionItem
                        const details = (0, docsFormatter_1.getDetails)(option.specs);
                        let priority;
                        if (isAlias(option)) {
                            priority = priorityMap.aliasOption;
                        }
                        else if (option.specs.required) {
                            priority = priorityMap.requiredOption;
                        }
                        else {
                            priority = priorityMap.option;
                        }
                        const completionItem = {
                            label: option.name,
                            detail: details,
                            // using index preserves order from the specification
                            // except when overridden by the priority
                            sortText: priority.toString() + index.toString().padStart(3),
                            kind: isAlias(option)
                                ? vscode_languageserver_1.CompletionItemKind.Reference
                                : vscode_languageserver_1.CompletionItemKind.Property,
                            documentation: (0, docsFormatter_1.formatOption)(option.specs),
                            data: {
                                documentUri: document.uri,
                                type: option.specs.type,
                                range: nodeRange,
                                atEndOfLine: cursorAtEndOfLine,
                                firstElementOfList: cursorAtFirstElementOfList,
                            },
                        };
                        const insertText = atEndOfLine(document, position)
                            ? `${option.name}:`
                            : option.name;
                        if (nodeRange) {
                            completionItem.textEdit = {
                                range: nodeRange,
                                newText: insertText,
                            };
                        }
                        else {
                            completionItem.insertText = insertText;
                        }
                        return completionItem;
                    });
                }
                // Now check if we're looking for option/sub-option values
                let keyPath;
                // establish path for the key (option/sub-option name)
                if (new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getValue() === null) {
                    keyPath = new yaml_2.AncestryBuilder(path)
                        .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                        .parent(yaml_1.YAMLMap)
                        .getKeyPath();
                }
                else {
                    // in this case there is a character immediately after DUMMY MAPPING, which
                    // prevents formation of nested map
                    keyPath = new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getKeyPath();
                }
                if (keyPath) {
                    const keyNode = keyPath[keyPath.length - 1];
                    const keyOptions = yield (0, yaml_2.getPossibleOptionsForPath)(keyPath, document, docsLibrary);
                    if (keyOptions &&
                        (0, yaml_1.isScalar)(keyNode) &&
                        keyOptions.has(keyNode.value)) {
                        const nodeRange = getNodeRange(node, document);
                        const option = keyOptions.get(keyNode.value);
                        const choices = [];
                        let defaultChoice = option.default;
                        if (option.type === "bool" && typeof option.default === "string") {
                            // the YAML parser does not recognize values such as 'Yes'/'no' as booleans
                            defaultChoice =
                                option.default.toLowerCase() === "yes" ? true : false;
                        }
                        if (option.choices) {
                            choices.push(...option.choices);
                        }
                        else if (option.type === "bool") {
                            choices.push(true);
                            choices.push(false);
                        }
                        else if (defaultChoice !== undefined) {
                            choices.push(defaultChoice);
                        }
                        return choices.map((choice, index) => {
                            let priority;
                            if (choice === defaultChoice) {
                                priority = priorityMap.defaultChoice;
                            }
                            else {
                                priority = priorityMap.choice;
                            }
                            const insertValue = new String(choice).toString();
                            const completionItem = {
                                label: insertValue,
                                detail: choice === defaultChoice ? "default" : undefined,
                                // using index preserves order from the specification
                                // except when overridden by the priority
                                sortText: priority.toString() + index.toString().padStart(3),
                                kind: vscode_languageserver_1.CompletionItemKind.Value,
                            };
                            if (nodeRange) {
                                completionItem.textEdit = {
                                    range: nodeRange,
                                    newText: insertValue,
                                };
                            }
                            else {
                                completionItem.insertText = insertValue;
                            }
                            return completionItem;
                        });
                    }
                }
                // check for 'hosts' keyword and 'ansible_host keyword under vars' to provide inventory auto-completion
                let keyPathForHosts;
                if (new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getValue() &&
                    new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getValue()["value"] === null) {
                    keyPathForHosts = new yaml_2.AncestryBuilder(path)
                        .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                        .parent(yaml_1.YAMLMap)
                        .getKeyPath();
                }
                else {
                    keyPathForHosts = new yaml_2.AncestryBuilder(path)
                        .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                        .getKeyPath();
                }
                if (keyPathForHosts) {
                    const keyNodeForHosts = keyPathForHosts[keyPathForHosts.length - 1];
                    const conditionForHostsKeyword = (0, yaml_2.isPlayParam)(keyPathForHosts) && keyNodeForHosts["value"] === "hosts";
                    const conditionForAnsibleHostKeyword = keyNodeForHosts["value"] === "ansible_host" &&
                        new yaml_2.AncestryBuilder(keyPathForHosts)
                            .parent()
                            .parent(yaml_1.YAMLMap)
                            .getStringKey() === "vars";
                    if (conditionForHostsKeyword || conditionForAnsibleHostKeyword) {
                        // const nodeRange = getNodeRange(node, document);
                        // nodeRange is not being passed to getHostCompletion because this will prevent
                        // completion for items beyond ',', ':', '!', and we know that 'hosts' keyword supports regex
                        const hostsList = (yield context.ansibleInventory).hostList;
                        const testHostCompletion = getHostCompletion(hostsList);
                        return testHostCompletion;
                    }
                }
            }
        }
        return null;
    });
}
exports.doCompletion = doCompletion;
function getKeywordCompletion(document, position, path, keywords) {
    const parameterMap = new yaml_2.AncestryBuilder(path)
        .parent(yaml_1.YAMLMap)
        .get();
    // find options that have been already provided by the user
    const providedParams = new Set((0, yaml_2.getYamlMapKeys)(parameterMap));
    const remainingParams = [...keywords.entries()].filter(([keyword]) => !providedParams.has(keyword));
    const nodeRange = getNodeRange(path[path.length - 1], document);
    return remainingParams.map(([keyword, description]) => {
        const priority = keyword === "name" ? priorityMap.nameKeyword : priorityMap.keyword;
        const completionItem = {
            label: keyword,
            kind: vscode_languageserver_1.CompletionItemKind.Property,
            sortText: `${priority}_${keyword}`,
            documentation: description,
        };
        const insertText = atEndOfLine(document, position)
            ? `${keyword}:`
            : keyword;
        if (nodeRange) {
            completionItem.textEdit = {
                range: nodeRange,
                newText: insertText,
            };
        }
        else {
            completionItem.insertText = insertText;
        }
        return completionItem;
    });
}
function getHostCompletion(hostObjectList) {
    return hostObjectList.map(({ host, priority }) => {
        const completionItem = {
            label: host,
            sortText: `${priority}_${host}`,
            kind: [1, 2].includes(priority)
                ? vscode_languageserver_1.CompletionItemKind.Variable
                : vscode_languageserver_1.CompletionItemKind.Value,
        };
        return completionItem;
    });
}
/**
 * Returns an LSP formatted range compensating for the DUMMY MAPPING hack, provided that
 * the node has range information and is a string scalar.
 */
function getNodeRange(node, document) {
    const range = (0, yaml_2.getOrigRange)(node);
    if (range && (0, yaml_1.isScalar)(node) && typeof node.value === "string") {
        const start = range[0];
        let end = range[1];
        // compensate for DUMMY MAPPING
        if (node.value.includes(dummyMappingCharacter)) {
            end -= 2;
        }
        else {
            // colon, being at the end of the line, was excluded from the node
            end -= 1;
        }
        return (0, misc_1.toLspRange)([start, end], document);
    }
}
function doCompletionResolve(completionItem, context) {
    var _a, _b, _c, _d;
    return __awaiter(this, void 0, void 0, function* () {
        if (((_a = completionItem.data) === null || _a === void 0 ? void 0 : _a.moduleFqcn) && ((_b = completionItem.data) === null || _b === void 0 ? void 0 : _b.documentUri)) {
            // resolve completion for a module
            const docsLibrary = yield context.docsLibrary;
            const [module] = yield docsLibrary.findModule(completionItem.data.moduleFqcn);
            if (module && module.documentation) {
                const [namespace, collection, name] = completionItem.data.moduleFqcn.split(".");
                let useFqcn = (yield context.documentSettings.get(completionItem.data.documentUri)).ansible.useFullyQualifiedCollectionNames;
                if (!useFqcn) {
                    // determine if the short name can really be used
                    const declaredCollections = ((_c = completionItem.data) === null || _c === void 0 ? void 0 : _c.inlineCollections) || [];
                    declaredCollections.push("ansible.builtin");
                    const metadata = yield context.documentMetadata.get(completionItem.data.documentUri);
                    if (metadata) {
                        declaredCollections.push(...metadata.collections);
                    }
                    const canUseShortName = declaredCollections.some((c) => c === `${namespace}.${collection}`);
                    if (!canUseShortName) {
                        // not an Ansible built-in module, and not part of the declared
                        // collections
                        useFqcn = true;
                    }
                }
                const insertName = useFqcn ? completionItem.data.moduleFqcn : name;
                const insertText = completionItem.data.atEndOfLine
                    ? `${insertName}:${resolveSuffix("dict", // since a module is always a dictionary
                    completionItem.data.firstElementOfList, isAnsiblePlaybook)}`
                    : insertName;
                if (completionItem.textEdit) {
                    completionItem.textEdit.newText = insertText;
                    completionItem.insertTextFormat = vscode_languageserver_1.InsertTextFormat.Snippet;
                }
                else {
                    completionItem.insertText = insertText;
                    completionItem.insertTextFormat = vscode_languageserver_1.InsertTextFormat.PlainText;
                }
                completionItem.documentation = (0, docsFormatter_1.formatModule)(module.documentation, docsLibrary.getModuleRoute(completionItem.data.moduleFqcn));
            }
        }
        if ((_d = completionItem.data) === null || _d === void 0 ? void 0 : _d.type) {
            // resolve completion for a module option or sub-option
            const insertText = completionItem.data.atEndOfLine
                ? `${completionItem.label}:${resolveSuffix(completionItem.data.type, completionItem.data.firstElementOfList, isAnsiblePlaybook)}`
                : `${completionItem.label}`;
            if (completionItem.textEdit) {
                completionItem.textEdit.newText = insertText;
            }
            else {
                completionItem.insertText = insertText;
            }
        }
        return completionItem;
    });
}
exports.doCompletionResolve = doCompletionResolve;
function isAlias(option) {
    return option.name !== option.specs.name;
}
function atEndOfLine(document, position) {
    const charAfterCursor = `${document.getText()}\n`[document.offsetAt(position)];
    return charAfterCursor === "\n" || charAfterCursor === "\r";
}
/**
 * A utility function to check if the item is the first element of a list or not
 * @param document current document
 * @param nodeRange range of the keyword in the document
 * @returns {boolean} true if the key is the first element of the list, else false
 */
function firstElementOfList(document, nodeRange) {
    const checkNodeRange = {
        start: { line: nodeRange.start.line, character: 0 },
        end: nodeRange.start,
    };
    const elementsBeforeKey = document.getText(checkNodeRange).trim();
    return elementsBeforeKey === "-";
}
function resolveSuffix(optionType, firstElementOfList, isDocPlaybook) {
    let returnSuffix;
    if (isDocPlaybook) {
        // if doc is a playbook, indentation will shift one tab since a play is a list
        switch (optionType) {
            case "list":
                returnSuffix = firstElementOfList ? `${os_1.EOL}\t\t- ` : `${os_1.EOL}\t- `;
                break;
            case "dict":
                returnSuffix = firstElementOfList ? `${os_1.EOL}\t\t` : `${os_1.EOL}\t`;
                break;
            default:
                returnSuffix = " ";
                break;
        }
    }
    else {
        // if doc is not a playbook (any other ansible file like task file, etc.) indentation will not
        // include that extra tab
        switch (optionType) {
            case "list":
                returnSuffix = `${os_1.EOL}\t- `;
                break;
            case "dict":
                returnSuffix = `${os_1.EOL}\t`;
                break;
            default:
                returnSuffix = " ";
                break;
        }
    }
    return returnSuffix;
}
exports.resolveSuffix = resolveSuffix;
//# sourceMappingURL=completionProvider.js.map