// React Native WebView cannot load external JS files, however it can load
// arbitrary JS via the injectedJavaScript property. So we use this to load external
// files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.

import { dirname, extname, basename } from 'path';

// We need this to be transpiled to `const webpack = require('webpack')`.
// As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop
import * as webpack from 'webpack';
import copyJs from './copyJs';

export default class BundledFile {
	private readonly bundleOutputPath: string;
	private readonly bundleBaseName: string;
	private readonly rootFileDirectory: string;

	public constructor(
		public readonly bundleName: string,
		private readonly sourceFilePath: string,
	) {
		this.rootFileDirectory = dirname(sourceFilePath);
		this.bundleBaseName = basename(sourceFilePath, extname(sourceFilePath));
		this.bundleOutputPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.js`;
	}

	private getWebpackOptions(mode: 'production' | 'development'): webpack.Configuration {
		const config: webpack.Configuration = {
			mode,
			entry: this.sourceFilePath,

			// es5: Have Webpack's generated code target ES5. This doesn't apply to code not
			//      generated by Webpack.
			target: ['web', 'es5'],

			output: {
				path: this.rootFileDirectory,
				filename: `${this.bundleBaseName}.bundle.js`,

				library: {
					type: 'window',
					name: this.bundleName,
				},
			},
			// See https://webpack.js.org/guides/typescript/
			module: {
				rules: [
					{
						// Include .tsx to include react components
						test: /\.tsx?$/,
						use: 'ts-loader',
						exclude: /node_modules/,
					},
					{
						test: (value) => {
							const isModuleFile = !!(/node_modules/.exec(value));

							// Some libraries don't work with older browsers/WebViews.
							// Because Babel transpilation can be slow, we only transpile
							// these libraries.
							// For now, it's just Replit's CodeMirror-vim library. This library
							// uses `a?.b` syntax, which seems to be unsupported in iOS 12 Safari.
							const moduleNeedsTranspilation = !!(/.*node_modules.*replit.*\.[mc]?js$/.exec(value));

							if (isModuleFile && !moduleNeedsTranspilation) {
								return false;
							}

							const isJsFile = !!(/\.[cm]?js$/.exec(value));
							if (isJsFile) {
								console.log('Compiling with Babel:', value);
							}
							return isJsFile;
						},
						use: {
							loader: 'babel-loader',
							options: {
								cacheDirectory: false,

								// Disable using babel.config.js to prevent conflicts with React Native's
								// Babel configuration.
								babelrc: false,
								configFile: false,

								presets: [
									['@babel/preset-env', { targets: { ios: 12, chrome: 80 } }],
								],
							},
						},
					},
				],
			},
			// Increase the minimum size required
			// to trigger warnings.
			// See https://stackoverflow.com/a/53517149/17055750
			performance: {
				maxAssetSize: 2_000_000, // 2-ish MiB
				maxEntrypointSize: 2_000_000,
			},
			resolve: {
				extensions: ['.tsx', '.ts', '.js'],

				// Some of these are used by the plugin background script
				fallback: {
					path: require.resolve('path-browserify'),
					events: require.resolve('events/'),
				},
			},
			cache: {
				type: 'filesystem',
			},
		};

		return config;
	}

	// Creates a file that can be imported by React native. This file contains the
	// bundled JS as a string.
	private async copyToImportableFile() {
		await copyJs(`${this.bundleName}.bundle`, this.bundleOutputPath);
	}

	private handleErrors(error: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
		let failed = false;

		if (error) {
			console.error(`Error (${this.bundleName}): ${error.name}`, error.message, error.stack);
			failed = true;
		} else if (stats?.hasErrors() || stats?.hasWarnings()) {
			const data = stats.toJson();

			if (data.warnings && data.warningsCount) {
				console.warn(`Warnings (${this.bundleName}): `, data.warningsCount);
				for (const warning of data.warnings) {
					// Stack contains the message
					if (warning.stack) {
						console.warn(warning.stack);
					} else {
						console.warn(warning.message);
					}
				}
			}
			if (data.errors && data.errorsCount) {
				console.error(`Errors (${this.bundleName}): `, data.errorsCount);
				for (const error of data.errors) {
					if (error.stack) {
						console.error(error.stack);
					} else {
						console.error(error.message);
					}
					console.error();
				}

				failed = true;
			}
		}

		return failed;
	}

	// Create a minified JS file in the same directory as `this.sourceFilePath` with
	// the same name.
	public build() {
		const compiler = webpack(this.getWebpackOptions('production'));
		return new Promise<void>((resolve, reject) => {
			console.info(`Building bundle: ${this.bundleName}...`);

			compiler.run((buildError, stats) => {
				// Always output stats, even on success
				console.log(`Bundle ${this.bundleName} built: `, stats?.toString());

				let failed = this.handleErrors(buildError, stats);

				// Clean up.
				compiler.close(async (closeError) => {
					if (closeError) {
						console.error('Error cleaning up:', closeError);
						failed = true;
					}

					let copyError;
					if (!failed) {
						try {
							await this.copyToImportableFile();
						} catch (error) {
							console.error('Error copying', error);
							failed = true;
							copyError = error;
						}
					}

					if (!failed) {
						resolve();
					} else {
						reject(closeError ?? buildError ?? copyError);
					}
				});
			});
		});
	}

	public startWatching() {
		const compiler = webpack(this.getWebpackOptions('development'));
		const watchOptions = {
			ignored: '**/node_modules',
		};

		console.info('Watching bundle: ', this.bundleName);
		compiler.watch(watchOptions, async (error, stats) => {
			const failed = this.handleErrors(error, stats);
			if (!failed) {
				await this.copyToImportableFile();
			}
		});
	}
}
