/**
 * @license
 * Copyright 2019-2020 Balena Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { spawn } from 'child_process';
import * as _ from 'lodash';
import * as path from 'path';
import * as shellEscape from 'shell-escape';

export const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
export const ROOT = path.join(__dirname, '..');

export function loadPackageJson() {
	return require(path.join(ROOT, 'package.json'));
}

/**
 * Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
 * as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
 */
export function fixPathForMsys(p: string): string {
	return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
}

/**
 * Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
 * The given argv arguments are escaped using the 'shell-escape' package,
 * so that backslashes in Windows paths, and other bash-special characters,
 * are preserved. If argv is not provided, defaults to process.argv, to the
 * effect that this current (parent) process is re-executed under MSYS2 bash.
 * This is useful to change the default shell from cmd.exe to MSYS2 bash on
 * Windows.
 * @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
 */
export async function runUnderMsys(argv?: string[]) {
	const newArgv = argv || process.argv;
	await new Promise((resolve, reject) => {
		const args = ['-lc', shellEscape(newArgv)];
		const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
		child.on('close', (code) => {
			if (code) {
				console.log(`runUnderMsys: child process exited with code ${code}`);
				reject(code);
			} else {
				resolve();
			}
		});
	});
}

/**
 * Run the executable at execPath as a child process, and resolve a promise
 * to the executable's stdout output as a string. Reject the promise if
 * anything is printed to stderr, or if the child process exits with a
 * non-zero exit code.
 * @param execPath Executable path
 * @param args Command-line argument for the executable
 */
export async function getSubprocessStdout(
	execPath: string,
	args: string[],
): Promise<string> {
	const child = spawn(execPath, args);
	return new Promise((resolve, reject) => {
		let stdout = '';
		child.stdout.on('error', reject);
		child.stderr.on('error', reject);
		child.stdout.on('data', (data: Buffer) => {
			try {
				stdout = data.toString();
			} catch (err) {
				reject(err);
			}
		});
		child.stderr.on('data', (data: Buffer) => {
			try {
				const stderr = data.toString();

				// ignore any debug lines, but ensure that we parse
				// every line provided to the stderr stream
				const lines = _.filter(
					stderr.trim().split(/\r?\n/),
					(line) => !line.startsWith('[debug]'),
				);
				if (lines.length > 0) {
					reject(
						new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
					);
				}
			} catch (err) {
				reject(err);
			}
		});
		child.on('exit', (code: number) => {
			if (code) {
				reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
			} else {
				resolve(stdout);
			}
		});
	});
}

/**
 * Error handling wrapper around the npm `which` package:
 * "Like the unix which utility. Finds the first instance of a specified
 * executable in the PATH environment variable. Does not cache the results,
 * so hash -r is not needed when the PATH changes."
 *
 * @param program Basename of a program, for example 'ssh'
 * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
 */
export async function which(program: string): Promise<string> {
	const whichMod = await import('which');
	let programPath: string;
	try {
		programPath = await whichMod(program);
	} catch (err) {
		if (err.code === 'ENOENT') {
			throw new Error(`'${program}' program not found. Is it installed?`);
		}
		throw err;
	}
	return programPath;
}

/**
 * Call which(programName) and spawn() with the given arguments. Throw an error
 * if the process exit code is not zero.
 */
export async function whichSpawn(
	programName: string,
	args: string[],
): Promise<void> {
	const program = await which(programName);
	let error: Error | undefined;
	let exitCode: number | undefined;
	try {
		exitCode = await new Promise<number>((resolve, reject) => {
			try {
				spawn(program, args, { stdio: 'inherit' })
					.on('error', reject)
					.on('close', resolve);
			} catch (err) {
				reject(err);
			}
		});
	} catch (err) {
		error = err;
	}
	if (error || exitCode) {
		const msg = [
			`${programName} failed with exit code ${exitCode}:`,
			`"${program}" [${args}]`,
		];
		if (error) {
			msg.push(`${error}`);
		}
		throw new Error(msg.join('\n'));
	}
}