const fs = require('fs-extra');
const execa = require('execa');

const utils = {};

utils.isLinux = () => {
	return process && process.platform === 'linux';
};

utils.isWindows = () => {
	return process && process.platform === 'win32';
};

utils.isMac = () => {
	return process && process.platform === 'darwin';
};

utils.execCommandVerbose = function(commandName, args = []) {
	console.info(`> ${commandName}`, args && args.length ? args : '');
	const promise = execa(commandName, args);
	promise.stdout.pipe(process.stdout);
	promise.stderr.pipe(process.stderr);
	return promise;
};

utils.execCommand = function(command) {
	const exec = require('child_process').exec;

	return new Promise((resolve, reject) => {
		exec(command, { maxBuffer: 1024 * 1024 }, (error, stdout) => {
			if (error) {

				// Special case for robocopy, which will return non-zero error codes
				// when sucessful. Doc is very imprecise but <= 7 seems more or less
				// fine and >= 8 seems more errorish. https://ss64.com/nt/robocopy-exit.html
				if (command.indexOf('robocopy') === 0 && error.code <= 7) {
					resolve(stdout.trim());
					return;
				}

				if (error.signal === 'SIGTERM') {
					resolve('Process was killed');
				} else {
					const newError = new Error(`Code: ${error.code}: ${error.message}: ${stdout.trim()}`);
					reject(newError);
				}
			} else {
				resolve(stdout.trim());
			}
		});
	});
};

utils.dirname = function(path) {
	if (!path) throw new Error('Path is empty');
	const s = path.split(/\/|\\/);
	s.pop();
	return s.join('/');
};

utils.basename = function(path) {
	if (!path) throw new Error('Path is empty');
	const s = path.split(/\/|\\/);
	return s[s.length - 1];
};

utils.filename = function(path, includeDir = false) {
	if (!path) throw new Error('Path is empty');
	let output = includeDir ? path : utils.basename(path);
	if (output.indexOf('.') < 0) return output;

	output = output.split('.');
	output.pop();
	return output.join('.');
};

utils.toSystemSlashes = function(path) {
	if (utils.isWindows()) return path.replace(/\//g, '\\');
	return path.replace(/\\/g, '/');
};

utils.fileExtension = function(path) {
	if (!path) throw new Error('Path is empty');

	const output = path.split('.');
	if (output.length <= 1) return '';
	return output[output.length - 1];
};

utils.replaceFileText = async function(filePath, regex, toInsert) {
	const content = await fs.readFile(filePath, 'utf8');
	const newContent = content.replace(regex, toInsert);
	if (newContent === content) return Promise.resolve();
	await fs.writeFile(filePath, newContent);
};

utils.copyDir = async function(src, dest, options) {
	options = Object.assign({}, {
		excluded: [],
		delete: true,
	}, options);

	src = utils.toSystemSlashes(src);
	dest = utils.toSystemSlashes(dest);

	await utils.mkdir(dest);

	if (utils.isWindows()) {
		let cmd = ['robocopy'];
		cmd.push(`"${src}"`);
		cmd.push(`"${dest}"`);
		cmd.push('/e');
		cmd.push('/r:0');
		if (options.delete) cmd.push('/purge');

		if (options.excluded.length) {
			cmd.push('/xd');
			cmd = cmd.concat(options.excluded.map(p => `"${utils.toSystemSlashes(p)}"`).join(' '));
		}

		await utils.execCommand(cmd.join(' '));
	} else {
		let excludedFlag = '';
		if (options.excluded.length) {
			excludedFlag = options.excluded.map(f => {
				return `--exclude "${f}"`;
			}).join(' ');
		}

		let deleteFlag = '';
		if (options.delete) deleteFlag = '--delete';

		await utils.execCommand(`rsync -a ${deleteFlag} ${excludedFlag} "${src}/" "${dest}/"`);
	}
};

// Occasionally, fs.mkdirp throws a "EEXIST" error if the directory already
// exists, while it should actually ignore the error. So we have this wrapper
// that actually handle the error. It means in general this method should be
// preferred to avoid random failures on CI or when building the app.
//
// https://github.com/laurent22/joplin/issues/6935#issuecomment-1274404470
utils.mkdir = async function(dir) {
	if (utils.isWindows()) {
		return utils.execCommand(`if not exist "${utils.toSystemSlashes(dir)}" mkdir "${utils.toSystemSlashes(dir)}"`);
	} else {
		try {
			// Can't return right away, or the exception won't be caught
			const result = await fs.mkdirp(dir);
			return result;
		} catch (error) {
			// Shouldn't happen but sometimes does. So we ignore the error in
			// this case.
			if (error.code === 'EEXIST') return;
			throw error;
		}
	}
};

utils.mkdirp = async function(dir) {
	return utils.mkdir(dir);
};

utils.copyFile = async function(src, dest) {
	await fs.copy(src, dest);
};

utils.rootDir = function() {
	return utils.dirname(utils.dirname(utils.dirname(__dirname)));
};

utils.registerGulpTasks = function(gulp, tasks) {
	for (const taskName in tasks) {
		gulp.task(taskName, tasks[taskName].fn);
	}
};

utils.setPackagePrivateField = async function(filePath, value) {
	const text = await fs.readFile(filePath, 'utf8');
	const obj = JSON.parse(text);
	if (!value) {
		delete obj.private;
	} else {
		obj.private = true;
	}
	await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
};

utils.insertContentIntoFile = async (filePath, marker, contentToInsert, createIfNotExist = false) => {
	const fs = require('fs-extra');
	const fileExists = await fs.pathExists(filePath);

	if (!fileExists) {
		if (!createIfNotExist) throw new Error(`File not found: ${filePath}`);
		await fs.writeFile(filePath, `${marker}\n${contentToInsert}\n${marker}`);
	} else {
		let content = await fs.readFile(filePath, 'utf-8');
		// [^]* matches any character including new lines
		const regex = new RegExp(`${marker}[^]*?${marker}`);
		content = content.replace(regex, `${marker}\n${contentToInsert}\n${marker}`);
		await fs.writeFile(filePath, content);
	}
};

utils.getFilename = (path) => {
	const lastPart = path.split('/').pop();
	if (lastPart.indexOf('.') < 0) return lastPart;

	const splitted = lastPart.split('.');
	splitted.pop();
	return splitted.join('.');
};

utils.msleep = (ms) => {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve();
		}, ms);
	});
};

module.exports = utils;