// @ts-check
// *****************************************************************************
// Copyright (C) 2021 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

const levenshtein = require('js-levenshtein');

// eslint-disable-next-line import/no-extraneous-dependencies
const metadata = require('@theia/core/src/common/i18n/nls.metadata.json');
const messages = new Set(Object.values(metadata.messages)
    .reduceRight((prev, curr) => prev.concat(curr), [])
    .map(e => e.replace(/&&/g, '')));

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
    meta: {
        type: 'problem',
        fixable: 'code',
        docs: {
            description: 'prevent incorrect use of \'nls.localize\'.',
        },
    },
    create(context) {
        return {
            CallExpression(node) {
                const callee = node.callee;
                if (callee.type === 'Super') {
                    return;
                }
                const localizeResults = evaluateLocalize(node);
                for (const { value, byDefault, node: localizeNode } of localizeResults) {
                    if (value !== undefined && localizeNode) {
                        if (byDefault && !messages.has(value)) {
                            let lowestDistance = Number.MAX_VALUE;
                            let lowestMessage = '';
                            for (const message of messages) {
                                const distance = levenshtein(value, message);
                                if (distance < lowestDistance) {
                                    lowestDistance = distance;
                                    lowestMessage = message;
                                }
                            }
                            if (lowestMessage) {
                                const replacementValue = `'${lowestMessage.replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
                                context.report({
                                    node: localizeNode,
                                    message: `'${value}' is not a valid default value. Did you mean ${replacementValue}?`,
                                    fix: function (fixer) {
                                        return fixer.replaceText(localizeNode, replacementValue);
                                    }
                                });
                            } else {
                                context.report({
                                    node: localizeNode,
                                    message: `'${value}' is not a valid default value.`
                                });
                            }
                        } else if (!byDefault && messages.has(value)) {
                            context.report({
                                node,
                                message: `'${value}' can be translated using the 'nls.localizeByDefault' function.`,
                                fix: function (fixer) {
                                    const code = context.getSourceCode();
                                    const args = node.arguments.slice(1);
                                    const argsCode = args.map(e => code.getText(e)).join(', ');
                                    const updatedCall = `nls.localizeByDefault(${argsCode})`;
                                    return fixer.replaceText(node, updatedCall);
                                }
                            });
                        }
                    }
                }
            }
        };

        /**
         * Evaluates a call expression and returns localization info.
         * @param {import('estree').CallExpression} node
         * @returns {Array<{value?: string, byDefault: boolean, node?: import('estree').Node}>}
         */
        function evaluateLocalize(/** @type {import('estree').CallExpression} */ node) {
            const callee = node.callee;
            if ('object' in callee && 'name' in callee.object && 'property' in callee && 'name' in callee.property && callee.object.name === 'nls') {
                if (callee.property.name === 'localize') {
                    const defaultTextNode = node.arguments[1]; // The default text node is the second argument for `nls.localize`
                    if (defaultTextNode && defaultTextNode.type === 'Literal' && typeof defaultTextNode.value === 'string') {
                        return [{
                            node: defaultTextNode,
                            value: defaultTextNode.value,
                            byDefault: false
                        }];
                    }
                } else if (callee.property.name === 'localizeByDefault') {
                    const defaultTextNode = node.arguments[0]; // The default text node is the first argument for `nls.localizeByDefault`
                    if (defaultTextNode && defaultTextNode.type === 'Literal' && typeof defaultTextNode.value === 'string') {
                        return [{
                            node: defaultTextNode,
                            value: defaultTextNode.value,
                            byDefault: true
                        }];
                    }
                }
            }
            // Check for Command.toDefaultLocalizedCommand
            if ('object' in callee && 'name' in callee.object && 'property' in callee && 'name' in callee.property
                && callee.object.name === 'Command' && callee.property.name === 'toDefaultLocalizedCommand') {
                const commandArg = node.arguments[0];
                if (commandArg && commandArg.type === 'ObjectExpression') {
                    return extractDefaultLocalizedProperties(commandArg);
                }
            }
            return [];
        }

        /**
         * Extracts label and category properties from a Command object that will be passed to localizeByDefault.
         * @param {import('estree').ObjectExpression} objectNode
         * @returns {Array<{value?: string, byDefault: boolean, node?: import('estree').Node}>}
         */
        function extractDefaultLocalizedProperties(objectNode) {
            const results = [];
            for (const property of objectNode.properties) {
                if (property.type === 'Property' && property.key.type === 'Identifier') {
                    const keyName = property.key.name;
                    if ((keyName === 'label' || keyName === 'category') && property.value.type === 'Literal' && typeof property.value.value === 'string') {
                        results.push({
                            node: property.value,
                            value: property.value.value,
                            byDefault: true
                        });
                    }
                }
            }
            return results;
        }
    }
};
