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

import fs from 'fs-extra';
import {
	AliasFromType,
	AliasToType,
	getAliasToType,
} from 'liferay-npm-build-tools-common/lib/alias';
import {
	BundlerPluginParams,
	BundlerTransformPluginState,
} from 'liferay-npm-build-tools-common/lib/api/plugins';
import FilePath from 'liferay-npm-build-tools-common/lib/file-path';
import PluginLogger from 'liferay-npm-build-tools-common/lib/plugin-logger';
import project from 'liferay-npm-build-tools-common/lib/project';

import {readAliases} from './config';
import {UnrolledAliasesMap, moduleExists, unrollAliasesMap} from './util';

/**
 * Plugin entry point
 */
export default function (
	params: BundlerPluginParams,
	_state: BundlerTransformPluginState
): void {
	const {log, pkg} = params;

	const absRootDir = project.dir.join(pkg.dir);

	const aliasesMap = readAliases(params, absRootDir);

	const unrolledAliasesMap = unrollAliasesMap(aliasesMap);

	reportAndResolveCollisions(log, absRootDir, unrolledAliasesMap);

	processAliases(log, absRootDir, unrolledAliasesMap);
}

/**
 *
 * @param log
 * @param absRootDir
 * @param unrolledAliasesMap should be filtered so that there's only one alias
 *        per entry.
 */
function processAliases(
	log: PluginLogger,
	absRootDir: FilePath,
	unrolledAliasesMap: UnrolledAliasesMap
) {
	Object.entries(unrolledAliasesMap).forEach(
		([absFromPath, unrolledAliases]) => {

			// Sanity check

			if (unrolledAliases.length > 1) {
				throw new Error(
					'Unrolled aliases map has unresolved collisions'
				);
			}

			const alias = unrolledAliases[0];

			const absFromFile = new FilePath(absFromPath);
			const rootRelAbsDirPosixPath = absRootDir.relative(alias.absDir)
				.asPosix;

			const rootRelFilePosixPath = absRootDir.relative(absFromFile)
				.asPosix;

			switch (getAliasToType(alias.to)) {
				case AliasToType.IGNORE: {
					rewriteFile(
						absFromFile,
						`/* ignored by alias field(s) configured in ${rootRelAbsDirPosixPath} */`
					);

					log.info(
						'replace-browser-modules',
						`Emptied file '${rootRelFilePosixPath}' as ` +
							`configured in '${rootRelAbsDirPosixPath}'`
					).linkToCode(1);
					break;
				}

				case AliasToType.EXTERNAL: {
					rewriteFile(
						absFromFile,
						`/* redirected by alias field(s) in ${rootRelAbsDirPosixPath} */`,
						`module.exports = require('${alias.to}');`
					);

					log.info(
						'replace-browser-modules',
						`Redirected file '${rootRelFilePosixPath}' to ` +
							`'${alias.to}' as configured in ` +
							`'${rootRelAbsDirPosixPath}'`
					).linkToCode(2);
					break;
				}

				case AliasToType.LOCAL: {
					const absToFile = alias.absDir.join(
						new FilePath(alias.to as string, {posix: true})
					);

					if (!absToFile.is(absFromFile)) {
						const fromRelToFile = absFromFile
							.dirname()
							.relative(absToFile);

						rewriteFile(
							absFromFile,
							`/* redirected by alias field(s) in ${rootRelAbsDirPosixPath} */`,
							`module.exports = require('./${fromRelToFile.asPosix}');`
						);

						log.info(
							'replace-browser-modules',
							`Redirected file '${rootRelFilePosixPath}' to ` +
								`'./${fromRelToFile.asPosix}' as configured in ` +
								`'${rootRelAbsDirPosixPath}'`
						).linkToCode(2);
					}

					break;
				}

				default:
					break;
			}
		}
	);
}

export function reportAndResolveCollisions(
	log: PluginLogger,
	absRootDir: FilePath,
	unrolledAliasesMap: UnrolledAliasesMap
): void {

	// Remove aliases out of ancestry line

	Object.keys(unrolledAliasesMap).forEach((absFromPosixPath) => {
		unrolledAliasesMap[absFromPosixPath] = unrolledAliasesMap[
			absFromPosixPath
		].filter((alias) => {
			const included = absFromPosixPath.startsWith(alias.absDir.asPosix);

			// No need to log anything because this type of alias must be
			// addressed by rewriting requires, not by module rewrite

			return included;
		});
	});

	// Remove aliases of external modules that would overwrite a local one

	Object.keys(unrolledAliasesMap).forEach((absFromPosixPath) => {
		unrolledAliasesMap[absFromPosixPath] = unrolledAliasesMap[
			absFromPosixPath
		].filter((alias) => {
			const included =
				alias.fromType !== AliasFromType.EXTERNAL ||
				!moduleExists(absFromPosixPath);

			if (!included) {
				const rootRelDir = absRootDir.relative(alias.absDir);
				const where =
					rootRelDir.asPosix === ''
						? "project's root folder"
						: `'${rootRelDir.asPosix}'`;

				log.warn(
					'replace-browser-modules',
					`Alias '${alias.from}' configured in ${where} will not ` +
						`be visible from outside because a local module with ` +
						`the same name exists`
				).linkToCode(3);
			}

			return included;
		});
	});

	// Remove empty aliases

	Object.keys(unrolledAliasesMap).forEach((absFromPath) => {
		if (!unrolledAliasesMap[absFromPath].length) {
			delete unrolledAliasesMap[absFromPath];
		}
	});

	// Resolve collisions in multiple length aliases

	Object.entries(unrolledAliasesMap)
		.filter(([_absFromPath, unrolledAliases]) => unrolledAliases.length > 1)
		.forEach(([absFromPath, unrolledAliases]) => {

			// Sort by distance to absFromPath

			unrolledAliases.sort(
				(a, b) => a.absDir.asPosix.length - b.absDir.asPosix.length
			);

			// we always use the last

			unrolledAliases.splice(0, unrolledAliases.length - 1);

			const alias = unrolledAliases[0];
			const rootRelFromPosixPath = absRootDir.relative(absFromPath)
				.asPosix;

			const rootRelDir = absRootDir.relative(alias.absDir);
			const where =
				rootRelDir.asPosix === ''
					? "project's root folder"
					: `'${rootRelDir.asPosix}'`;

			log.warn(
				'replace-browser-modules',
				`File '${rootRelFromPosixPath}' is aliased more than once, ` +
					`only the alias configured in ${where} will be visible ` +
					`when required from outside`
			).linkToCode(4);
		});
}

function rewriteFile(absFile: FilePath, ...lines: string[]) {
	fs.ensureDirSync(absFile.dirname().asNative);
	fs.writeFileSync(absFile.asNative, lines.join('\n'));
}
