/**
 * @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 */
/** @typedef {import("../../types").Linter.LanguageOptions} JSLanguageOptions */

//-----------------------------------------------------------------------------
// 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 {JSLanguageOptions} 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,
		jsx: ecmaFeatures.jsx,
	});
}

/**
 * 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 {JSLanguageOptions} 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,
				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 {JSLanguageOptions} 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,
		});
	},
};
