"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vue = require("@volar/vue-language-core");
const shared_1 = require("@vue/shared");
const html = require("vscode-html-languageservice");
const vscode = require("vscode-languageserver-protocol");
const helpers_1 = require("../helpers");
const nameCasing_1 = require("../ideFeatures/nameCasing");
const types_1 = require("../types");
const data_1 = require("./data");
let builtInData;
let modelData;
function useVueTemplateLanguagePlugin(options) {
    const plugin = (_context) => {
        const templatePlugin = options.templateLanguagePlugin(_context);
        const triggerCharacters = {
            triggerCharacters: [
                ...templatePlugin.triggerCharacters ?? [],
                '@', // vue event shorthand
            ],
        };
        if (!_context?.typescript)
            return triggerCharacters;
        builtInData ??= (0, data_1.loadTemplateData)(_context.locale ?? 'en');
        modelData ??= (0, data_1.loadModelModifiersData)(_context.locale ?? 'en');
        // https://vuejs.org/api/built-in-directives.html#v-on
        // https://vuejs.org/api/built-in-directives.html#v-bind
        const eventModifiers = {};
        const propModifiers = {};
        const vOn = builtInData.globalAttributes?.find(x => x.name === 'v-on');
        const vBind = builtInData.globalAttributes?.find(x => x.name === 'v-bind');
        if (vOn) {
            const markdown = (typeof vOn.description === 'string' ? vOn.description : vOn.description?.value) ?? '';
            const modifiers = markdown
                .split('\n- ')[4]
                .split('\n').slice(2, -1);
            for (let text of modifiers) {
                text = text.substring('  - `.'.length);
                const [name, disc] = text.split('` - ');
                eventModifiers[name] = disc;
            }
        }
        if (vBind) {
            const markdown = (typeof vBind.description === 'string' ? vBind.description : vBind.description?.value) ?? '';
            const modifiers = markdown
                .split('\n- ')[4]
                .split('\n').slice(2, -1);
            for (let text of modifiers) {
                text = text.substring('  - `.'.length);
                const [name, disc] = text.split('` - ');
                propModifiers[name] = disc;
            }
        }
        const _ts = _context.typescript;
        return {
            ...templatePlugin,
            ...triggerCharacters,
            async provideCompletionItems(document, position, context, token) {
                if (!options.isSupportedDocument(document))
                    return;
                for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) {
                    const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root;
                    if (virtualFile && virtualFile instanceof vue.VueFile) {
                        await provideHtmlData(map, virtualFile);
                    }
                }
                const htmlComplete = await templatePlugin.provideCompletionItems?.(document, position, context, token);
                if (!htmlComplete)
                    return;
                for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) {
                    const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root;
                    if (virtualFile && virtualFile instanceof vue.VueFile) {
                        afterHtmlCompletion(htmlComplete, map, virtualFile);
                    }
                }
                return htmlComplete;
            },
            async provideInlayHints(document) {
                if (!options.isSupportedDocument(document))
                    return;
                const enabled = await _context.configurationHost?.getConfiguration('vue.inlayHints.missingProps') ?? false;
                if (!enabled)
                    return;
                const result = [];
                for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) {
                    const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root;
                    const scanner = options.getScanner(document, templatePlugin);
                    if (virtualFile && virtualFile instanceof vue.VueFile && scanner) {
                        // visualize missing required props
                        const casing = await (0, nameCasing_1.getNameCasing)(_context, _ts, map.sourceFileDocument.uri);
                        const nativeTags = (0, helpers_1.checkNativeTags)(_ts.module, _ts.languageService, virtualFile.fileName);
                        const components = (0, helpers_1.checkComponentNames)(_ts.module, _ts.languageService, virtualFile, nativeTags);
                        const componentProps = {};
                        let token;
                        let current;
                        while ((token = scanner.scan()) !== html.TokenType.EOS) {
                            if (token === html.TokenType.StartTag) {
                                const tagName = scanner.getTokenText();
                                const component = tagName.indexOf('.') >= 0
                                    ? components.find(component => component === tagName.split('.')[0])
                                    : components.find(component => component === tagName || (0, shared_1.hyphenate)(component) === tagName);
                                const checkTag = tagName.indexOf('.') >= 0 ? tagName : component;
                                if (checkTag) {
                                    componentProps[checkTag] ??= (0, helpers_1.checkPropsOfTag)(_ts.module, _ts.languageService, virtualFile, checkTag, nativeTags, true);
                                    current = {
                                        unburnedRequiredProps: [...componentProps[checkTag]],
                                        labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(),
                                        insertOffset: scanner.getTokenOffset() + scanner.getTokenLength(),
                                    };
                                }
                            }
                            else if (token === html.TokenType.AttributeName) {
                                if (current) {
                                    let attrText = scanner.getTokenText();
                                    if (attrText === 'v-bind') {
                                        current.unburnedRequiredProps = [];
                                    }
                                    else {
                                        // remove modifiers
                                        if (attrText.indexOf('.') >= 0) {
                                            attrText = attrText.split('.')[0];
                                        }
                                        // normalize
                                        if (attrText.startsWith('v-bind:')) {
                                            attrText = attrText.substring('v-bind:'.length);
                                        }
                                        else if (attrText.startsWith(':')) {
                                            attrText = attrText.substring(':'.length);
                                        }
                                        else if (attrText.startsWith('v-model:')) {
                                            attrText = attrText.substring('v-model:'.length);
                                        }
                                        else if (attrText === 'v-model') {
                                            attrText = options.vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName?
                                        }
                                        else if (attrText.startsWith('@')) {
                                            attrText = 'on-' + (0, shared_1.hyphenate)(attrText.substring('@'.length));
                                        }
                                        current.unburnedRequiredProps = current.unburnedRequiredProps.filter(propName => {
                                            return attrText !== propName
                                                && attrText !== (0, shared_1.hyphenate)(propName);
                                        });
                                    }
                                }
                            }
                            else if (token === html.TokenType.StartTagSelfClose || token === html.TokenType.StartTagClose) {
                                if (current) {
                                    for (const requiredProp of current.unburnedRequiredProps) {
                                        result.push({
                                            label: `${requiredProp}!`,
                                            paddingLeft: true,
                                            position: document.positionAt(current.labelOffset),
                                            kind: vscode.InlayHintKind.Parameter,
                                            textEdits: [{
                                                    range: {
                                                        start: document.positionAt(current.insertOffset),
                                                        end: document.positionAt(current.insertOffset),
                                                    },
                                                    newText: ` :${casing.attr === types_1.AttrNameCasing.Kebab ? (0, shared_1.hyphenate)(requiredProp) : requiredProp}=`,
                                                }],
                                        });
                                    }
                                    current = undefined;
                                }
                            }
                            if (token === html.TokenType.AttributeName || token === html.TokenType.AttributeValue) {
                                if (current) {
                                    current.insertOffset = scanner.getTokenOffset() + scanner.getTokenLength();
                                }
                            }
                        }
                    }
                }
                return result;
            },
            provideHover(document, position, token) {
                if (!options.isSupportedDocument(document))
                    return;
                if (_context.documents.isVirtualFileUri(document.uri))
                    templatePlugin.updateCustomData([]);
                return templatePlugin.provideHover?.(document, position, token);
            },
            async provideSyntacticDiagnostics(document, token) {
                if (!options.isSupportedDocument(document))
                    return;
                const originalResult = await templatePlugin.provideSyntacticDiagnostics?.(document, token);
                for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) {
                    const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root;
                    if (!virtualFile || !(virtualFile instanceof vue.VueFile))
                        continue;
                    const templateErrors = [];
                    const sfcVueTemplateCompiled = virtualFile.compiledSFCTemplate;
                    if (sfcVueTemplateCompiled) {
                        for (const error of sfcVueTemplateCompiled.errors) {
                            onCompilerError(error, vscode.DiagnosticSeverity.Error);
                        }
                        for (const warning of sfcVueTemplateCompiled.warnings) {
                            onCompilerError(warning, vscode.DiagnosticSeverity.Warning);
                        }
                        function onCompilerError(error, severity) {
                            const templateHtmlRange = {
                                start: error.loc?.start.offset ?? 0,
                                end: error.loc?.end.offset ?? 0,
                            };
                            let errorMessage = error.message;
                            templateErrors.push({
                                range: {
                                    start: document.positionAt(templateHtmlRange.start),
                                    end: document.positionAt(templateHtmlRange.end),
                                },
                                severity,
                                code: error.code,
                                source: 'vue',
                                message: errorMessage,
                            });
                        }
                    }
                    return [
                        ...originalResult ?? [],
                        ...templateErrors,
                    ];
                }
            },
            async provideDocumentSemanticTokens(document, range, legend, token) {
                if (!options.isSupportedDocument(document))
                    return;
                const result = await templatePlugin.provideDocumentSemanticTokens?.(document, range, legend, token) ?? [];
                const scanner = options.getScanner(document, templatePlugin);
                if (!scanner)
                    return;
                for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) {
                    const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root;
                    if (!virtualFile || !(virtualFile instanceof vue.VueFile))
                        continue;
                    const nativeTags = (0, helpers_1.checkNativeTags)(_ts.module, _ts.languageService, virtualFile.fileName);
                    const templateScriptData = (0, helpers_1.checkComponentNames)(_ts.module, _ts.languageService, virtualFile, nativeTags);
                    const components = new Set([
                        ...templateScriptData,
                        ...templateScriptData.map(shared_1.hyphenate),
                    ]);
                    const offsetRange = {
                        start: document.offsetAt(range.start),
                        end: document.offsetAt(range.end),
                    };
                    let token = scanner.scan();
                    while (token !== html.TokenType.EOS) {
                        const tokenOffset = scanner.getTokenOffset();
                        // TODO: fix source map perf and break in while condition
                        if (tokenOffset > offsetRange.end)
                            break;
                        if (tokenOffset >= offsetRange.start && (token === html.TokenType.StartTag || token === html.TokenType.EndTag)) {
                            const tokenText = scanner.getTokenText();
                            if (components.has(tokenText) || tokenText.indexOf('.') >= 0) {
                                const tokenLength = scanner.getTokenLength();
                                const tokenPosition = document.positionAt(tokenOffset);
                                if (components.has(tokenText)) {
                                    let tokenType = legend.tokenTypes.indexOf('component');
                                    if (tokenType === -1) {
                                        tokenType = legend.tokenTypes.indexOf('class');
                                    }
                                    result.push([tokenPosition.line, tokenPosition.character, tokenLength, tokenType, 0]);
                                }
                            }
                        }
                        token = scanner.scan();
                    }
                }
                return result;
            },
        };
        async function provideHtmlData(map, vueSourceFile) {
            const casing = await (0, nameCasing_1.getNameCasing)(_context, _ts, map.sourceFileDocument.uri);
            if (builtInData.tags) {
                for (const tag of builtInData.tags) {
                    if (tag.name === 'slot')
                        continue;
                    if (tag.name === 'component')
                        continue;
                    if (tag.name === 'template')
                        continue;
                    if (casing.tag === types_1.TagNameCasing.Kebab) {
                        tag.name = (0, shared_1.hyphenate)(tag.name);
                    }
                    else {
                        tag.name = (0, shared_1.camelize)((0, shared_1.capitalize)(tag.name));
                    }
                }
            }
            const nativeTags = (0, helpers_1.checkNativeTags)(_ts.module, _ts.languageService, vueSourceFile.fileName);
            templatePlugin.updateCustomData([
                html.newHTMLDataProvider('vue-template-built-in', builtInData),
                {
                    getId: () => 'vue-template',
                    isApplicable: () => true,
                    provideTags: () => {
                        const components = (0, helpers_1.checkComponentNames)(_ts.module, _ts.languageService, vueSourceFile, nativeTags)
                            .filter(name => name !== 'Transition'
                            && name !== 'TransitionGroup'
                            && name !== 'KeepAlive'
                            && name !== 'Suspense'
                            && name !== 'Teleport');
                        const scriptSetupRanges = vueSourceFile.sfc.scriptSetupAst ? vue.parseScriptSetupRanges(_ts.module, vueSourceFile.sfc.scriptSetupAst, options.vueCompilerOptions) : undefined;
                        const names = new Set();
                        const tags = [];
                        for (const tag of components) {
                            if (casing.tag === types_1.TagNameCasing.Kebab) {
                                names.add((0, shared_1.hyphenate)(tag));
                            }
                            else if (casing.tag === types_1.TagNameCasing.Pascal) {
                                names.add(tag);
                            }
                        }
                        for (const binding of scriptSetupRanges?.bindings ?? []) {
                            const name = vueSourceFile.sfc.scriptSetup.content.substring(binding.start, binding.end);
                            if (casing.tag === types_1.TagNameCasing.Kebab) {
                                names.add((0, shared_1.hyphenate)(name));
                            }
                            else if (casing.tag === types_1.TagNameCasing.Pascal) {
                                names.add(name);
                            }
                        }
                        for (const name of names) {
                            tags.push({
                                name: name,
                                attributes: [],
                            });
                        }
                        return tags;
                    },
                    provideAttributes: (tag) => {
                        const attrs = (0, helpers_1.getElementAttrs)(_ts.module, _ts.languageService, vueSourceFile.fileName, tag);
                        const props = new Set((0, helpers_1.checkPropsOfTag)(_ts.module, _ts.languageService, vueSourceFile, tag, nativeTags));
                        const events = (0, helpers_1.checkEventsOfTag)(_ts.module, _ts.languageService, vueSourceFile, tag, nativeTags);
                        const attributes = [];
                        for (const prop of [...props, ...attrs]) {
                            const isGlobal = !props.has(prop);
                            const name = casing.attr === types_1.AttrNameCasing.Camel ? prop : (0, shared_1.hyphenate)(prop);
                            if ((0, shared_1.hyphenate)(name).startsWith('on-')) {
                                const propNameBase = name.startsWith('on-')
                                    ? name.slice('on-'.length)
                                    : (name['on'.length].toLowerCase() + name.slice('onX'.length));
                                const propKey = createInternalItemId('componentEvent', [isGlobal ? '*' : tag, propNameBase]);
                                attributes.push({
                                    name: 'v-on:' + propNameBase,
                                    description: propKey,
                                }, {
                                    name: '@' + propNameBase,
                                    description: propKey,
                                });
                            }
                            {
                                const propName = name;
                                const propKey = createInternalItemId('componentProp', [isGlobal ? '*' : tag, propName]);
                                attributes.push({
                                    name: propName,
                                    description: propKey,
                                }, {
                                    name: ':' + propName,
                                    description: propKey,
                                }, {
                                    name: 'v-bind:' + propName,
                                    description: propKey,
                                });
                            }
                        }
                        for (const event of events) {
                            const name = casing.attr === types_1.AttrNameCasing.Camel ? event : (0, shared_1.hyphenate)(event);
                            const propKey = createInternalItemId('componentEvent', [tag, name]);
                            attributes.push({
                                name: 'v-on:' + name,
                                description: propKey,
                            });
                            attributes.push({
                                name: '@' + name,
                                description: propKey,
                            });
                        }
                        const models = [];
                        for (const prop of [...props, ...attrs]) {
                            if (prop.startsWith('onUpdate:')) {
                                const isGlobal = !props.has(prop);
                                models.push([isGlobal, prop.substring('onUpdate:'.length)]);
                            }
                        }
                        for (const event of events) {
                            if (event.startsWith('update:')) {
                                models.push([false, event.substring('update:'.length)]);
                            }
                        }
                        for (const [isGlobal, model] of models) {
                            const name = casing.attr === types_1.AttrNameCasing.Camel ? model : (0, shared_1.hyphenate)(model);
                            const propKey = createInternalItemId('componentProp', [isGlobal ? '*' : tag, name]);
                            attributes.push({
                                name: 'v-model:' + name,
                                description: propKey,
                            });
                            if (model === 'modelValue') {
                                attributes.push({
                                    name: 'v-model',
                                    description: propKey,
                                });
                            }
                        }
                        return attributes;
                    },
                    provideValues: () => [],
                },
            ]);
        }
        function afterHtmlCompletion(completionList, map, vueSourceFile) {
            const replacement = getReplacement(completionList, map.sourceFileDocument);
            const nativeTags = (0, helpers_1.checkNativeTags)(_ts.module, _ts.languageService, vueSourceFile.fileName);
            const componentNames = new Set((0, helpers_1.checkComponentNames)(_ts.module, _ts.languageService, vueSourceFile, nativeTags).map(shared_1.hyphenate));
            if (replacement) {
                const isEvent = replacement.text.startsWith('v-on:') || replacement.text.startsWith('@');
                const isProp = replacement.text.startsWith('v-bind:') || replacement.text.startsWith(':');
                const isModel = replacement.text.startsWith('v-model:') || replacement.text.split('.')[0] === 'v-model';
                const hasModifier = replacement.text.includes('.');
                const validModifiers = isEvent ? eventModifiers
                    : isProp ? propModifiers
                        : undefined;
                const modifiers = replacement.text.split('.').slice(1);
                const textWithoutModifier = replacement.text.split('.')[0];
                if (validModifiers && hasModifier) {
                    for (const modifier in validModifiers) {
                        if (modifiers.includes(modifier))
                            continue;
                        const modifierDes = validModifiers[modifier];
                        const insertText = textWithoutModifier + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier;
                        const newItem = {
                            label: modifier,
                            filterText: insertText,
                            documentation: {
                                kind: 'markdown',
                                value: modifierDes,
                            },
                            textEdit: {
                                range: replacement.textEdit.range,
                                newText: insertText,
                            },
                            kind: vscode.CompletionItemKind.EnumMember,
                        };
                        completionList.items.push(newItem);
                    }
                }
                else if (hasModifier && isModel) {
                    for (const modifier of modelData.globalAttributes ?? []) {
                        if (modifiers.includes(modifier.name))
                            continue;
                        const insertText = textWithoutModifier + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier.name;
                        const newItem = {
                            label: modifier.name,
                            filterText: insertText,
                            documentation: {
                                kind: 'markdown',
                                value: (typeof modifier.description === 'object' ? modifier.description.value : modifier.description)
                                    + '\n\n' + modifier.references?.map(ref => `[${ref.name}](${ref.url})`).join(' | '),
                            },
                            textEdit: {
                                range: replacement.textEdit.range,
                                newText: insertText,
                            },
                            kind: vscode.CompletionItemKind.EnumMember,
                        };
                        completionList.items.push(newItem);
                    }
                }
            }
            for (const item of completionList.items) {
                const itemIdKey = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value;
                const itemId = itemIdKey ? readInternalItemId(itemIdKey) : undefined;
                if (itemId) {
                    item.documentation = undefined;
                }
                if (itemIdKey && itemId) {
                    if (itemId.type === 'componentProp' || itemId.type === 'componentEvent') {
                        const [componentName] = itemId.args;
                        if (componentName !== '*') {
                            item.sortText = '\u0000' + (item.sortText ?? item.label);
                        }
                        if (itemId.type === 'componentProp') {
                            if (componentName !== '*') {
                                item.kind = vscode.CompletionItemKind.Field;
                            }
                        }
                        else {
                            item.kind = componentName !== '*' ? vscode.CompletionItemKind.Function : vscode.CompletionItemKind.Event;
                        }
                    }
                    else if (item.label === 'v-if'
                        || item.label === 'v-else-if'
                        || item.label === 'v-else'
                        || item.label === 'v-for') {
                        item.kind = vscode.CompletionItemKind.Method;
                        item.sortText = '\u0003' + (item.sortText ?? item.label);
                    }
                    else if (item.label.startsWith('v-')) {
                        item.kind = vscode.CompletionItemKind.Function;
                        item.sortText = '\u0002' + (item.sortText ?? item.label);
                    }
                    else {
                        item.sortText = '\u0001' + (item.sortText ?? item.label);
                    }
                }
                else if (item.kind === vscode.CompletionItemKind.Property && componentNames.has((0, shared_1.hyphenate)(item.label))) {
                    item.kind = vscode.CompletionItemKind.Variable;
                    item.sortText = '\u0000' + (item.sortText ?? item.label);
                }
            }
            templatePlugin.updateCustomData([]);
        }
    };
    return plugin;
}
exports.default = useVueTemplateLanguagePlugin;
function createInternalItemId(type, args) {
    return '__VLS_::' + type + '::' + args.join(',');
}
function readInternalItemId(key) {
    if (key.startsWith('__VLS_::')) {
        const strs = key.split('::');
        return {
            type: strs[1],
            args: strs[2].split(','),
        };
    }
}
function getReplacement(list, doc) {
    for (const item of list.items) {
        if (item.textEdit && 'range' in item.textEdit) {
            return {
                item: item,
                textEdit: item.textEdit,
                text: doc.getText(item.textEdit.range)
            };
        }
    }
}
//# sourceMappingURL=vue-template.js.map