/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

// Increase max listeners for event emitters
import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 100;

import es from 'event-stream';
import fancyLog from 'fancy-log';
import glob from 'glob';
import gulp from 'gulp';
import filter from 'gulp-filter';
import plumber from 'gulp-plumber';
import sourcemaps from 'gulp-sourcemaps';
import * as path from 'path';
import * as nodeUtil from 'util';
import * as ext from './lib/extensions.ts';
import { getVersion } from './lib/getVersion.ts';
import { createReporter } from './lib/reporter.ts';
import * as task from './lib/task.ts';
import * as tsb from './lib/tsb/index.ts';
import { createTsgoStream, spawnTsgo } from './lib/tsgo.ts';
import * as util from './lib/util.ts';
import watcher from './lib/watch/index.ts';

const root = path.dirname(import.meta.dirname);
const commit = getVersion(root);

// Tracks active extension compilations to emit aggregate
// "Starting compilation" / "Finished compilation" messages
// that the problem matcher in tasks.json relies on.
let activeExtensionCompilations = 0;

function onExtensionCompilationStart(): void {
	if (activeExtensionCompilations === 0) {
		fancyLog('Starting compilation');
	}
	activeExtensionCompilations++;
}

function onExtensionCompilationEnd(): void {
	activeExtensionCompilations--;
	if (activeExtensionCompilations === 0) {
		fancyLog('Finished compilation');
	}
}

// To save 250ms for each gulp startup, we are caching the result here
// const compilations = glob.sync('**/tsconfig.json', {
// 	cwd: extensionsPath,
// 	ignore: ['**/out/**', '**/node_modules/**']
// });
const compilations = [
	'extensions/configuration-editing/tsconfig.json',
	'extensions/css-language-features/client/tsconfig.json',
	'extensions/css-language-features/server/tsconfig.json',
	'extensions/debug-auto-launch/tsconfig.json',
	'extensions/debug-server-ready/tsconfig.json',
	'extensions/emmet/tsconfig.json',
	'extensions/extension-editing/tsconfig.json',
	'extensions/git/tsconfig.json',
	'extensions/git-base/tsconfig.json',
	'extensions/github/tsconfig.json',
	'extensions/github-authentication/tsconfig.json',
	'extensions/grunt/tsconfig.json',
	'extensions/gulp/tsconfig.json',
	'extensions/html-language-features/client/tsconfig.json',
	'extensions/html-language-features/server/tsconfig.json',
	'extensions/ipynb/tsconfig.json',
	'extensions/jake/tsconfig.json',
	'extensions/json-language-features/client/tsconfig.json',
	'extensions/json-language-features/server/tsconfig.json',
	'extensions/markdown-language-features/tsconfig.json',
	'extensions/markdown-math/tsconfig.json',
	'extensions/media-preview/tsconfig.json',
	'extensions/merge-conflict/tsconfig.json',
	'extensions/mermaid-chat-features/tsconfig.json',
	'extensions/terminal-suggest/tsconfig.json',
	'extensions/microsoft-authentication/tsconfig.json',
	'extensions/notebook-renderers/tsconfig.json',
	'extensions/npm/tsconfig.json',
	'extensions/php-language-features/tsconfig.json',
	'extensions/references-view/tsconfig.json',
	'extensions/search-result/tsconfig.json',
	'extensions/simple-browser/tsconfig.json',
	'extensions/tunnel-forwarding/tsconfig.json',
	'extensions/typescript-language-features/web/tsconfig.json',
	'extensions/typescript-language-features/tsconfig.json',
	'extensions/vscode-api-tests/tsconfig.json',
	'extensions/vscode-colorize-tests/tsconfig.json',
	'extensions/vscode-colorize-perf-tests/tsconfig.json',
	'extensions/vscode-test-resolver/tsconfig.json',

	'.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json',
	'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
	'.vscode/extensions/vscode-extras/tsconfig.json',
];

const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;

function rewriteTsgoSourceMappingUrlsIfNeeded(build: boolean, out: string, baseUrl: string): Promise<void> {
	if (!build) {
		return Promise.resolve();
	}

	return util.streamToPromise(
		gulp.src(path.join(out, '**', '*.js'), { base: out })
			.pipe(util.rewriteSourceMappingURL(baseUrl))
			.pipe(gulp.dest(out))
	);
}

const tasks = compilations.map(function (tsconfigFile) {
	const absolutePath = path.join(root, tsconfigFile);
	const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, ''));

	const overrideOptions: { sourceMap?: boolean; inlineSources?: boolean; base?: string } = {};
	overrideOptions.sourceMap = true;

	const name = relativeDirname.replace(/\//g, '-');

	const srcRoot = path.dirname(tsconfigFile);
	const srcBase = path.join(srcRoot, 'src');
	const src = path.join(srcBase, '**');
	const srcOpts = { cwd: root, base: srcBase, dot: true };

	const out = path.join(srcRoot, 'out');
	const baseUrl = getBaseUrl(out);

	function createPipeline(build: boolean, emitError?: boolean, transpileOnly?: boolean) {
		const reporter = createReporter('extensions');

		overrideOptions.inlineSources = Boolean(build);
		overrideOptions.base = path.dirname(absolutePath);

		const compilation = tsb.create(absolutePath, overrideOptions, { verbose: false, transpileOnly, transpileOnlyIncludesDts: transpileOnly, transpileWithEsbuild: true }, err => reporter(err.toString()));

		const pipeline = function () {
			const input = es.through();
			const tsFilter = filter(['**/*.ts', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true, dot: true });
			const output = input
				.pipe(plumber({
					errorHandler: function (err) {
						if (err && !err.__reporter__) {
							reporter(err);
						}
					}
				}))
				.pipe(tsFilter)
				.pipe(util.loadSourcemaps())
				.pipe(compilation())
				.pipe(build ? util.stripSourceMappingURL() : es.through())
				.pipe(sourcemaps.write('.', {
					sourceMappingURL: !build ? undefined : f => `${baseUrl}/${f.relative}.map`,
					addComment: !!build,
					includeContent: !!build,
					// note: trailing slash is important, else the source URLs in V8's file coverage are incorrect
					sourceRoot: '../src/',
				}))
				.pipe(tsFilter.restore)
				.pipe(reporter.end(!!emitError));

			return es.duplex(input, output);
		};

		// add src-stream for project files
		pipeline.tsProjectSrc = () => {
			return compilation.src(srcOpts);
		};
		return pipeline;
	}

	const cleanTask = task.define(`clean-extension-${name}`, util.rimraf(out));

	const transpileTask = task.define(`transpile-extension:${name}`, task.series(cleanTask, () => {
		const pipeline = createPipeline(false, true, true);
		const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts']));
		const input = es.merge(nonts, pipeline.tsProjectSrc());

		return input
			.pipe(pipeline())
			.pipe(gulp.dest(out));
	}));

	const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => {
		const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true }));
		const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out)));
		const tsgo = spawnTsgo(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));

		await Promise.all([copyNonTs, tsgo]);
	}));

	const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => {
		const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true }));
		const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } });
		const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out));
		const tsgoStream = watchInput.pipe(util.debounce(() => {
			onExtensionCompilationStart();
			const stream = createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));
			// Wrap in a result stream that always emits 'end' (even on
			// error) so the debounce resets to idle and can process future
			// file changes. Errors from tsgo (e.g. type errors causing a
			// non-zero exit code) are already reported by spawnTsgo's
			// runReporter, so swallowing the stream error is safe.
			const result = es.through();
			stream.on('end', () => {
				onExtensionCompilationEnd();
				result.emit('end');
			});
			stream.on('error', () => {
				onExtensionCompilationEnd();
				result.emit('end');
			});
			return result;
		}, 200));
		const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream);

		return watchStream;
	}));

	// Tasks
	gulp.task(transpileTask);
	gulp.task(compileTask);
	gulp.task(watchTask);

	return { transpileTask, compileTask, watchTask };
});

const transpileExtensionsTask = task.define('transpile-extensions', task.parallel(...tasks.map(t => t.transpileTask)));
gulp.task(transpileExtensionsTask);

export const compileExtensionsTask = task.define('compile-extensions', task.parallel(...tasks.map(t => t.compileTask)));
gulp.task(compileExtensionsTask);

export const watchExtensionsTask = task.define('watch-extensions', task.parallel(...tasks.map(t => t.watchTask)));
gulp.task(watchExtensionsTask);

//#region Extension media

export const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false));
gulp.task(compileExtensionMediaTask);

export const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true));
gulp.task(watchExtensionMedia);

export const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions'));
gulp.task(compileExtensionMediaBuildTask);

//#endregion

//#region Azure Pipelines

/**
 * Cleans the build directory for extensions
 */
export const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions'));

/**
 * brings in the marketplace extensions for the build
 */
const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build')));

/**
 * Compiles the non-native extensions for the build
 * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that.
 */
export const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series(
	bundleMarketplaceExtensionsBuildTask,
	task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build')))
));
gulp.task(compileNonNativeExtensionsBuildTask);

/**
 * Compiles the native extensions for the build
 * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that.
 */
export const compileNativeExtensionsBuildTask = task.define('compile-native-extensions-build', () => ext.packageNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build')));
gulp.task(compileNativeExtensionsBuildTask);

/**
 * Compiles the extensions for the build.
 * This is essentially a helper task that combines {@link cleanExtensionsBuildTask}, {@link compileNonNativeExtensionsBuildTask} and {@link compileNativeExtensionsBuildTask}
 */
export const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series(
	cleanExtensionsBuildTask,
	bundleMarketplaceExtensionsBuildTask,
	task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))),
));
gulp.task(compileAllExtensionsBuildTask);



//#endregion

export const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false));
gulp.task(compileWebExtensionsTask);

export const watchWebExtensionsTask = task.define('watch-web', () => buildWebExtensions(true));
gulp.task(watchWebExtensionsTask);

async function buildWebExtensions(isWatch: boolean): Promise<void> {
	const extensionsPath = path.join(root, 'extensions');

	// Find all esbuild.browser.mts files
	const esbuildConfigLocations = await nodeUtil.promisify(glob)(
		path.join(extensionsPath, '**', 'esbuild.browser.mts'),
		{ ignore: ['**/node_modules'] }
	);

	const promises: Promise<unknown>[] = [];

	// Esbuild for extensions
	if (esbuildConfigLocations.length > 0) {
		promises.push(
			ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script }))),
			// Also run type check on extensions
			...esbuildConfigLocations.flatMap(script => {
				const roots = ext.getBuildRootsForExtension(path.dirname(script));
				return roots.map(root => ext.typeCheckExtension(root, true));
			})
		);
	}

	await Promise.all(promises);
}
