/**
 * SPDX-FileCopyrightText: © 2020 Liferay, Inc. <https://liferay.com>
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

import {
	AliasToType,
	AliasToValue,
	getAliasFields,
	getAliasToType,
	loadAliases,
} from 'liferay-npm-build-tools-common/lib/alias';
import {BabelIpcObject} from 'liferay-npm-build-tools-common/lib/api/plugins';
import * as babelIpc from 'liferay-npm-build-tools-common/lib/babel-ipc';
import FilePath from 'liferay-npm-build-tools-common/lib/file-path';
import * as mod from 'liferay-npm-build-tools-common/lib/modules';
import PluginLogger from 'liferay-npm-build-tools-common/lib/plugin-logger';
import project from 'liferay-npm-build-tools-common/lib/project';

const absPrjDirPath = project.dir.asNative;
const absBuildDirPath = project.dir.join(project.buildDir).asNative;
let t;

export class Visitor {
	constructor(state) {
		const babelIpcObject: BabelIpcObject = babelIpc.get(state, {});

		this._aliasFields = getAliasFields(
			babelIpcObject.globalConfig,
			state.opts
		);
		this._absFile = new FilePath(state.file.opts.filename);
		this._log = babelIpcObject.log;

		this._absRootDir = new FilePath(
			this._absFile.asNative.startsWith(absBuildDirPath)
				? absBuildDirPath
				: absPrjDirPath
		);

		state.visitor = this;
	}

	CallExpression(bpath) {
		const {_log} = this;
		const {node} = bpath;
		const {callee} = node;

		if (!t.isIdentifier(callee)) {
			return;
		}

		if (callee.name !== 'require') {
			return;
		}

		const argument = node.arguments[0];

		if (!t.isLiteral(argument) || !argument.value) {
			return;
		}

		const moduleName = argument.value;

		if (typeof moduleName !== 'string') {
			return;
		}

		const aliasedModuleName = this._resolve(moduleName);

		if (aliasedModuleName === false) {
			bpath.replaceWith(t.objectExpression([]));

			_log.info(
				'alias-modules',
				`Replaced require('${moduleName}') with {}`
			).linkToCode(5);
		}
		else if (aliasedModuleName !== argument.value) {
			argument.value = aliasedModuleName;

			_log.info(
				'alias-modules',
				`Redirected '${moduleName}' to ` + `'${aliasedModuleName}'`
			).linkToCode(6);
		}
	}

	/**
	 * Get required module name as it should be required from `absFile` or `false`
	 * if it should be ignored.
	 *
	 * @param requiredModuleName
	 */
	_resolve(requiredModuleName: string): AliasToValue {
		const {_absFile, _absRootDir, _aliasFields, _log} = this;

		// Fail for absolute path modules

		if (requiredModuleName.startsWith('/')) {
			_log.error(
				'babel-plugin-alias-modules',
				`Require to absolute path ${requiredModuleName} will not work in ` +
					`AMD environments (like Liferay)`
			);

			return undefined;
		}

		const absFileDir = _absFile.dirname();
		const requiredModule = new FilePath(requiredModuleName, {posix: true});

		let alias;

		if (mod.isLocalModule(requiredModuleName)) {

			// First look in file directory (without recursion)

			alias = this._getAliasForLocal(
				absFileDir,
				absFileDir,
				requiredModule
			);

			// Then, if not found, recursively from target module up

			if (alias === undefined) {
				const moduleDir = absFileDir.join(requiredModule.dirname());
				const moduleBasename = requiredModule.basename();

				alias = this._getAliasForLocal(
					_absRootDir,
					moduleDir,
					moduleBasename
				);

				if (
					alias !== undefined &&
					getAliasToType(alias) === AliasToType.LOCAL
				) {
					alias = absFileDir
						.relative(moduleDir)
						.join(new FilePath(alias, {posix: true})).asPosix;

					if (!alias.startsWith('.')) {
						alias = `./${alias}`;
					}
				}
			}
		}
		else {
			alias = this._getAliasForExternal(absFileDir, requiredModule);
		}

		return alias === undefined ? requiredModuleName : alias;
	}

	/**
	 * Get resolved module name as it should be required from `absSearchDir` or
	 * `undefined` if it is not aliased.
	 *
	 * @param searchRelModuleName (without leading `./`)
	 */
	private _getAliasForLocal(
		absSearchTopDir: FilePath,
		absSearchDir: FilePath,
		searchRelModuleName: FilePath
	): AliasToValue | undefined {
		const {_aliasFields} = this;

		const aliases = loadAliases(
			absSearchDir.join('package.json'),
			_aliasFields
		);
		const normalizedSearchRelModuleName = `./${searchRelModuleName.asPosix}`;

		let alias = aliases[normalizedSearchRelModuleName];

		// Try with file alias

		if (
			alias === undefined &&
			!normalizedSearchRelModuleName.toLowerCase().endsWith('.js')
		) {
			alias = aliases[`${normalizedSearchRelModuleName}.js`];
		}

		// Try with external module aliases

		if (alias === undefined) {
			alias = aliases[searchRelModuleName.asPosix];
		}

		// Found: return it

		if (alias !== undefined) {
			return alias;
		}

		// Search finished

		if (absSearchDir.is(absSearchTopDir)) {
			return undefined;
		}

		// Look up in hierachy

		alias = this._getAliasForLocal(
			absSearchTopDir,
			absSearchDir.dirname(),
			absSearchDir.basename().join(searchRelModuleName)
		);

		// Rebase to current folder

		if (
			alias !== undefined &&
			getAliasToType(alias) === AliasToType.LOCAL
		) {
			alias = new FilePath(`../${alias}`, {posix: true}).normalize()
				.asPosix;
		}

		return alias;
	}

	/**
	 * Get resolved module name or `undefined` if it is not aliased.
	 *
	 * @param absRootDir
	 * @param absSearchDir
	 * @param searchRelModuleName
	 */
	private _getAliasForExternal(
		absSearchDir: FilePath,
		requiredModule: FilePath
	): AliasToValue | undefined {
		const {_absRootDir, _aliasFields} = this;

		const aliases = loadAliases(
			absSearchDir.join('package.json'),
			_aliasFields
		);

		let alias = aliases[requiredModule.asPosix];

		// Found: return it

		if (alias !== undefined) {
			return alias;
		}

		// Search finished

		if (absSearchDir.is(_absRootDir)) {
			return undefined;
		}

		// Look up in hierachy

		alias = this._getAliasForExternal(
			absSearchDir.dirname(),
			requiredModule
		);

		// Rebase to current folder

		if (
			alias !== undefined &&
			getAliasToType(alias) === AliasToType.LOCAL
		) {
			alias = new FilePath(`../${alias}`, {posix: true}).normalize()
				.asPosix;
		}

		return alias;
	}

	private readonly _absRootDir: FilePath;
	private readonly _aliasFields: string[];
	private readonly _absFile: FilePath;
	private readonly _log: PluginLogger;
}

/**
 * @return {object} a babel visitor
 */
export default function ({types}) {
	t = types;

	return {
		visitor: {
			CallExpression(bpath, state) {
				state.visitor.CallExpression(bpath);
			},
			Program(bpath, state) {
				new Visitor(state);
			},
		},
	};
}
