/**
 * @fileoverview JavaScript Language Object
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const { SourceCode } = require("./source-code");
const createDebug = require("debug");
const astUtils = require("../../shared/ast-utils");
const espree = require("espree");
const eslintScope = require("eslint-scope");
const evk = require("eslint-visitor-keys");
const { validateLanguageOptions } = require("./validate-language-options");
const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version");

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/** @typedef {import("@eslint/core").File} File */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").OkParseResult} OkParseResult */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const debug = createDebug("eslint:languages:js");
const DEFAULT_ECMA_VERSION = 5;
const parserSymbol = Symbol.for("eslint.RuleTester.parser");

/**
 * Analyze scope of the given AST.
 * @param {ASTNode} ast The `Program` node to analyze.
 * @param {LanguageOptions} languageOptions The parser options.
 * @param {Record<string, string[]>} visitorKeys The visitor keys.
 * @returns {ScopeManager} The analysis result.
 */
function analyzeScope(ast, languageOptions, visitorKeys) {
    const parserOptions = languageOptions.parserOptions;
    const ecmaFeatures = parserOptions.ecmaFeatures || {};
    const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;

    return eslintScope.analyze(ast, {
        ignoreEval: true,
        nodejsScope: ecmaFeatures.globalReturn,
        impliedStrict: ecmaFeatures.impliedStrict,
        ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
        sourceType: languageOptions.sourceType || "script",
        childVisitorKeys: visitorKeys || evk.KEYS,
        fallback: evk.getKeys
    });
}

/**
 * Determines if a given object is Espree.
 * @param {Object} parser The parser to check.
 * @returns {boolean} True if the parser is Espree or false if not.
 */
function isEspree(parser) {
    return !!(parser === espree || parser[parserSymbol] === espree);
}

/**
 * Normalize ECMAScript version from the initial config into languageOptions (year)
 * format.
 * @param {any} [ecmaVersion] ECMAScript version from the initial config
 * @returns {number} normalized ECMAScript version
 */
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) {

    switch (ecmaVersion) {
        case 3:
            return 3;

        // void 0 = no ecmaVersion specified so use the default
        case 5:
        case void 0:
            return 5;

        default:
            if (typeof ecmaVersion === "number") {
                return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009;
            }
    }

    /*
     * We default to the latest supported ecmaVersion for everything else.
     * Remember, this is for languageOptions.ecmaVersion, which sets the version
     * that is used for a number of processes inside of ESLint. It's normally
     * safe to assume people want the latest unless otherwise specified.
     */
    return LATEST_ECMA_VERSION;
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
 * @type {Language}
 */
module.exports = {

    fileType: "text",
    lineStart: 1,
    columnStart: 0,
    nodeTypeKey: "type",
    visitorKeys: evk.KEYS,

    defaultLanguageOptions: {
        sourceType: "module",
        ecmaVersion: "latest",
        parser: espree,
        parserOptions: {}
    },

    validateLanguageOptions,

    /**
     * Normalizes the language options.
     * @param {Object} languageOptions The language options to normalize.
     * @returns {Object} The normalized language options.
     */
    normalizeLanguageOptions(languageOptions) {

        languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
            languageOptions.ecmaVersion
        );

        // Espree expects this information to be passed in
        if (isEspree(languageOptions.parser)) {
            const parserOptions = languageOptions.parserOptions;

            if (languageOptions.sourceType) {

                parserOptions.sourceType = languageOptions.sourceType;

                if (
                    parserOptions.sourceType === "module" &&
                    parserOptions.ecmaFeatures &&
                    parserOptions.ecmaFeatures.globalReturn
                ) {
                    parserOptions.ecmaFeatures.globalReturn = false;
                }
            }
        }

        return languageOptions;

    },

    /**
     * Determines if a given node matches a given selector class.
     * @param {string} className The class name to check.
     * @param {ASTNode} node The node to check.
     * @param {Array<ASTNode>} ancestry The ancestry of the node.
     * @returns {boolean} True if there's a match, false if not.
     * @throws {Error} When an unknown class name is passed.
     */
    matchesSelectorClass(className, node, ancestry) {

        /*
         * Copyright (c) 2013, Joel Feenstra
         * All rights reserved.
         *
         * Redistribution and use in source and binary forms, with or without
         * modification, are permitted provided that the following conditions are met:
         *    * Redistributions of source code must retain the above copyright
         *      notice, this list of conditions and the following disclaimer.
         *    * Redistributions in binary form must reproduce the above copyright
         *      notice, this list of conditions and the following disclaimer in the
         *      documentation and/or other materials provided with the distribution.
         *    * Neither the name of the ESQuery nor the names of its contributors may
         *      be used to endorse or promote products derived from this software without
         *      specific prior written permission.
         *
         * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
         * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
         * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
         * DISCLAIMED. IN NO EVENT SHALL JOEL FEENSTRA BE LIABLE FOR ANY
         * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
         * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
         * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
         * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
         * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
         * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
         */

        switch (className.toLowerCase()) {

            case "statement":
                if (node.type.slice(-9) === "Statement") {
                    return true;
                }

                // fallthrough: interface Declaration <: Statement { }

            case "declaration":
                return node.type.slice(-11) === "Declaration";

            case "pattern":
                if (node.type.slice(-7) === "Pattern") {
                    return true;
                }

                // fallthrough: interface Expression <: Node, Pattern { }

            case "expression":
                return node.type.slice(-10) === "Expression" ||
                    node.type.slice(-7) === "Literal" ||
                    (
                        node.type === "Identifier" &&
                        (ancestry.length === 0 || ancestry[0].type !== "MetaProperty")
                    ) ||
                    node.type === "MetaProperty";

            case "function":
                return node.type === "FunctionDeclaration" ||
                    node.type === "FunctionExpression" ||
                    node.type === "ArrowFunctionExpression";

            default:
                throw new Error(`Unknown class name: ${className}`);
        }
    },

    /**
     * Parses the given file into an AST.
     * @param {File} file The virtual file to parse.
     * @param {Object} options Additional options passed from ESLint.
     * @param {LanguageOptions} options.languageOptions The language options.
     * @returns {Object} The result of parsing.
     */
    parse(file, { languageOptions }) {

        // Note: BOM already removed
        const { body: text, path: filePath } = file;
        const textToParse = text.replace(astUtils.shebangPattern, (match, captured) => `//${captured}`);
        const { ecmaVersion, sourceType, parser } = languageOptions;
        const parserOptions = Object.assign(
            { ecmaVersion, sourceType },
            languageOptions.parserOptions,
            {
                loc: true,
                range: true,
                raw: true,
                tokens: true,
                comment: true,
                eslintVisitorKeys: true,
                eslintScopeManager: true,
                filePath
            }
        );

        /*
         * Check for parsing errors first. If there's a parsing error, nothing
         * else can happen. However, a parsing error does not throw an error
         * from this method - it's just considered a fatal error message, a
         * problem that ESLint identified just like any other.
         */
        try {
            debug("Parsing:", filePath);
            const parseResult = (typeof parser.parseForESLint === "function")
                ? parser.parseForESLint(textToParse, parserOptions)
                : { ast: parser.parse(textToParse, parserOptions) };

            debug("Parsing successful:", filePath);

            const {
                ast,
                services: parserServices = {},
                visitorKeys = evk.KEYS,
                scopeManager
            } = parseResult;

            return {
                ok: true,
                ast,
                parserServices,
                visitorKeys,
                scopeManager
            };
        } catch (ex) {

            // If the message includes a leading line number, strip it:
            const message = ex.message.replace(/^line \d+:/iu, "").trim();

            debug("%s\n%s", message, ex.stack);

            return {
                ok: false,
                errors: [{
                    message,
                    line: ex.lineNumber,
                    column: ex.column
                }]
            };
        }

    },

    /**
     * Creates a new `SourceCode` object from the given information.
     * @param {File} file The virtual file to create a `SourceCode` object from.
     * @param {OkParseResult} parseResult The result returned from `parse()`.
     * @param {Object} options Additional options passed from ESLint.
     * @param {LanguageOptions} options.languageOptions The language options.
     * @returns {SourceCode} The new `SourceCode` object.
     */
    createSourceCode(file, parseResult, { languageOptions }) {

        const { body: text, path: filePath, bom: hasBOM } = file;
        const { ast, parserServices, visitorKeys } = parseResult;

        debug("Scope analysis:", filePath);
        const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys);

        debug("Scope analysis successful:", filePath);

        return new SourceCode({
            text,
            ast,
            hasBOM,
            parserServices,
            scopeManager,
            visitorKeys
        });
    }

};
