/**
 * Run this script to convert the project to TypeScript. This is only guaranteed to work
 * on the unmodified default template; if you have done code changes you are likely need
 * to touch up the generated project manually.
 */

// @ts-check
const fs = require('fs');
const path = require('path');
const { argv } = require('process');

const projectRoot = argv[2] || path.join(__dirname, '..');

const isRollup = fs.existsSync(path.join(projectRoot, "rollup.config.js"));

function warn(message) {
	console.warn('Warning: ' + message);
}

function replaceInFile(fileName, replacements) {
	if (fs.existsSync(fileName)) {
		let contents = fs.readFileSync(fileName, 'utf8');
		let hadUpdates = false;

		replacements.forEach(([from, to]) => {
			const newContents = contents.replace(from, to);

			const isAlreadyApplied = typeof to !== 'string' || contents.includes(to);

			if (newContents !== contents) {
				contents = newContents;
				hadUpdates = true;
			} else if (!isAlreadyApplied) {
				warn(`Wanted to update "${from}" in ${fileName}, but did not find it.`);
			}
		});

		if (hadUpdates) {
			fs.writeFileSync(fileName, contents);
		} else {
			console.log(`${fileName} had already been updated.`);
		}
	} else {
		warn(`Wanted to update ${fileName} but the file did not exist.`);
	}
}

function createFile(fileName, contents) {
	if (fs.existsSync(fileName)) {
		warn(`Wanted to create ${fileName}, but it already existed. Leaving existing file.`);
	} else {
		fs.writeFileSync(fileName, contents);
	}
}

function addDepsToPackageJson() {
	const pkgJSONPath = path.join(projectRoot, 'package.json');
	const packageJSON = JSON.parse(fs.readFileSync(pkgJSONPath, 'utf8'));
	packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
		...(isRollup ? { '@rollup/plugin-typescript': '^6.0.0' } : { 'ts-loader': '^8.0.4' }),
		'@tsconfig/svelte': '^1.0.10',
		'@types/compression': '^1.7.0',
		'@types/node': '^14.11.1',
		'@types/polka': '^0.5.1',
		'svelte-check': '^1.0.46',
		'svelte-preprocess': '^4.3.0',
		tslib: '^2.0.1',
		typescript: '^4.0.3'
	});

	// Add script for checking
	packageJSON.scripts = Object.assign(packageJSON.scripts, {
		validate: 'svelte-check --ignore src/node_modules/@sapper'
	});

	// Write the package JSON
	fs.writeFileSync(pkgJSONPath, JSON.stringify(packageJSON, null, '  '));
}

function changeJsExtensionToTs(dir) {
	const elements = fs.readdirSync(dir, { withFileTypes: true });

	for (let i = 0; i < elements.length; i++) {
		if (elements[i].isDirectory()) {
			changeJsExtensionToTs(path.join(dir, elements[i].name));
		} else if (elements[i].name.match(/^[^_]((?!json).)*js$/)) {
			fs.renameSync(path.join(dir, elements[i].name), path.join(dir, elements[i].name).replace('.js', '.ts'));
		}
	}
}

function updateSingleSvelteFile({ view, vars, contextModule }) {
	replaceInFile(path.join(projectRoot, 'src', `${view}.svelte`), [
		[/(?:<script)(( .*?)*?)>/gm, (m, attrs) => `<script${attrs}${!attrs.includes('lang="ts"') ? ' lang="ts"' : ''}>`],
		...(vars ? vars.map(({ name, type }) => [`export let ${name};`, `export let ${name}: ${type};`]) : []),
		...(contextModule ? contextModule.map(({ js, ts }) => [js, ts]) : [])
	]);
}

// Switch the *.svelte file to use TS
function updateSvelteFiles() {
	[
		{
			view: 'components/Nav',
			vars: [{ name: 'segment', type: 'string' }]
		},
		{
			view: 'routes/_layout',
			vars: [{ name: 'segment', type: 'string' }]
		},
		{
			view: 'routes/_error',
			vars: [
				{ name: 'status', type: 'number' },
				{ name: 'error', type: 'Error' }
			]
		},
		{
			view: 'routes/blog/index',
			vars: [{ name: 'posts', type: '{ slug: string; title: string, html: any }[]' }],
			contextModule: [
				{
					js: '.then(r => r.json())',
					ts: '.then((r: { json: () => any; }) => r.json())'
				},
				{
					js: '.then(posts => {',
					ts: '.then((posts: { slug: string; title: string, html: any }[]) => {'
				}
			]
		},
		{
			view: 'routes/blog/[slug]',
			vars: [{ name: 'post', type: '{ slug: string; title: string, html: any }' }]
		}
	].forEach(updateSingleSvelteFile);
}

function updateRollupConfig() {
	// Edit rollup config
	replaceInFile(path.join(projectRoot, 'rollup.config.js'), [
		// Edit imports
		[
			/'rollup-plugin-terser';\n(?!import sveltePreprocess)/,
			`'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
`
		],
		// Edit inputs
		[
			/(?<!THIS_IS_UNDEFINED[^\n]*\n\s*)onwarn\(warning\);/,
			`(warning.code === 'THIS_IS_UNDEFINED') ||\n\tonwarn(warning);`
		],
		[/input: config.client.input\(\)(?!\.replace)/, `input: config.client.input().replace(/\\.js$/, '.ts')`],
		[
			/input: config.server.input\(\)(?!\.replace)/,
			`input: { server: config.server.input().server.replace(/\\.js$/, ".ts") }`
		],
		[
			/input: config.serviceworker.input\(\)(?!\.replace)/,
			`input: config.serviceworker.input().replace(/\\.js$/, '.ts')`
		],
		// Add preprocess to the svelte config, this is tricky because there's no easy signifier.
		// Instead we look for 'hydratable: true,'
		[/hydratable: true(?!,\n\s*preprocess)/g, 'hydratable: true,\n\t\t\t\tpreprocess: sveltePreprocess()'],
		// Add TypeScript
		[/commonjs\(\)(?!,\n\s*typescript)/g, 'commonjs(),\n\t\t\ttypescript({ sourceMap: dev })']
	]);
}

function updateWebpackConfig() {
	// Edit webpack config
	replaceInFile(path.join(projectRoot, 'webpack.config.js'), [
		// Edit imports
		[
			/require\('webpack-modules'\);\n(?!const sveltePreprocess)/,
			`require('webpack-modules');\nconst sveltePreprocess = require('svelte-preprocess');\n`
		],
		// Edit extensions
		[
			/\['\.mjs', '\.js', '\.json', '\.svelte', '\.html'\]/,
			`['.mjs', '.js', '.ts', '.json', '.svelte', '.html']`
		],
		// Edit entries
		[
			/entry: config\.client\.entry\(\)/,
			`entry: { main: config.client.entry().main.replace(/\\.js$/, '.ts') }`
		],
		[
			/entry: config\.server\.entry\(\)/,
			`entry: { server: config.server.entry().server.replace(/\\.js$/, '.ts') }`
		],
		[
			/entry: config\.serviceworker\.entry\(\)/,
			`entry: { 'service-worker': config.serviceworker.entry()['service-worker'].replace(/\\.js$/, '.ts') }`
		],
		// Add preprocess to the svelte config, this is tricky because there's no easy signifier.
		// Instead we look for 'hydratable: true,'
		[
			/hydratable: true(?!,\n\s*preprocess)/g,
			'hydratable: true,\n\t\t\t\t\t\t\tpreprocess: sveltePreprocess()'
		],
		// Add TypeScript rules for client and server
		[
			/module: {\n\s*rules: \[\n\s*(?!{\n\s*test: \/\\\.ts\$\/)/g,
			`module: {\n\t\t\trules: [\n\t\t\t\t{\n\t\t\t\t\ttest: /\\.ts$/,\n\t\t\t\t\tloader: 'ts-loader'\n\t\t\t\t},\n\t\t\t\t`
		],
		// Add TypeScript rules for serviceworker
		[
			/output: config\.serviceworker\.output\(\),\n\s*(?!module)/,
			`output: config.serviceworker.output(),\n\t\tmodule: {\n\t\t\trules: [\n\t\t\t\t{\n\t\t\t\t\ttest: /\\.ts$/,\n\t\t\t\t\tloader: 'ts-loader'\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t`
		],
		// Edit outputs
		[
			/output: config\.serviceworker\.output\(\),\n\s*(?!resolve)/,
			`output: config.serviceworker.output(),\n\t\tresolve: { extensions: ['.mjs', '.js', '.ts', '.json'] },\n\t\t`
		]
	]);
}

function updateServiceWorker() {
	replaceInFile(path.join(projectRoot, 'src', 'service-worker.ts'), [
		[`shell.concat(files);`, `(shell as string[]).concat(files as string[]);`],
		[`self.skipWaiting();`, `((self as any) as ServiceWorkerGlobalScope).skipWaiting();`],
		[`self.clients.claim();`, `((self as any) as ServiceWorkerGlobalScope).clients.claim();`],
		[`fetchAndCache(request)`, `fetchAndCache(request: Request)`],
		[`self.addEventListener('activate', event =>`, `self.addEventListener('activate', (event: ExtendableEvent) =>`],
		[`self.addEventListener('install', event =>`, `self.addEventListener('install', (event: ExtendableEvent) =>`],
		[`addEventListener('fetch', event =>`, `addEventListener('fetch', (event: FetchEvent) =>`],
	]);
}

function createTsConfig() {
	const tsconfig = `{
		"extends": "@tsconfig/svelte/tsconfig.json",
		"compilerOptions": {
			"lib": ["DOM", "ES2017", "WebWorker"]
		},
		"include": ["src/**/*", "src/node_modules/**/*"],
		"exclude": ["node_modules/*", "__sapper__/*", "static/*"]
	}`;

	createFile(path.join(projectRoot, 'tsconfig.json'), tsconfig);
}

// Adds the extension recommendation
function configureVsCode() {
	const dir = path.join(projectRoot, '.vscode');

	if (!fs.existsSync(dir)) {
		fs.mkdirSync(dir);
	}

	createFile(path.join(projectRoot, '.vscode', 'extensions.json'), `{"recommendations": ["svelte.svelte-vscode"]}`);
}

function deleteThisScript() {
	fs.unlinkSync(path.join(__filename));

	// Check for Mac's DS_store file, and if it's the only one left remove it
	const remainingFiles = fs.readdirSync(path.join(__dirname));
	if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
		fs.unlinkSync(path.join(__dirname, '.DS_store'));
	}

	// Check if the scripts folder is empty
	if (fs.readdirSync(path.join(__dirname)).length === 0) {
		// Remove the scripts folder
		fs.rmdirSync(path.join(__dirname));
	}
}

console.log(`Adding TypeScript with ${isRollup ? "Rollup" : "webpack" }...`);

addDepsToPackageJson();

changeJsExtensionToTs(path.join(projectRoot, 'src'));

updateSvelteFiles();

if (isRollup) {
	updateRollupConfig();
} else {
	updateWebpackConfig();
}

updateServiceWorker();

createTsConfig();

configureVsCode();

// Delete this script, but not during testing
if (!argv[2]) {
	deleteThisScript();
}

console.log('Converted to TypeScript.');

if (fs.existsSync(path.join(projectRoot, 'node_modules'))) {
	console.log(`
Next:
1. run 'npm install' again to install TypeScript dependencies
2. run 'npm run build' for the @sapper imports in your project to work
`);
}
