import { promisify } from 'util';
import { resolve, join } from 'path';
import { existsSync, readdir, stat, watch as fsw } from 'fs';
import { exec } from 'child_process';

const toExec = promisify(exec);
const toStats = promisify(stat);
const toRead = promisify(readdir);

// modified: lukeed/totalist
async function walk(dir, callback, pre='') {
	await toRead(dir).then(arr => {
		return Promise.all(
			arr.map(str => {
				let abs = join(dir, str);
				return toStats(abs).then(stats => {
					if (!stats.isDirectory()) return;
					callback(join(pre, str), abs, stats);
					return walk(abs, callback, join(pre, str));
				});
			})
		);
	});
}

async function setup(dir, onChange) {
	let output = {};

	try {
		output[dir] = fsw(dir, { recursive: true }, onChange.bind(0, dir));
	} catch (err) {
		if (err.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') throw err;
		output[dir] = fsw(dir, onChange.bind(0, dir));
		await walk(dir, (rel, abs) => {
			output[abs] = fsw(abs, onChange.bind(0, abs));
		});
	}

	return output;
}

export async function watch(list, callback, opts={}) {
	const cwd = resolve('.', opts.cwd || '.');
	const dirs = new Set(list.map(str => resolve(cwd, str)).filter(existsSync));
	const ignores = ['node_modules'].concat(opts.ignore || []).map(x => new RegExp(x, 'i'));

	let wip = 0;
	const Triggers = new Set;
	const Watchers = new Map;

	async function handle() {
		await callback();
		if (--wip) return handle();
	}

	// TODO: Catch `EPERM` on Windows for removed dir
	async function onChange(dir, type, filename) {
		if (ignores.some(x => x.test(filename))) return;

		let tmp = join(dir, filename);
		if (Triggers.has(tmp)) return;
		if (wip++) return wip = 1;

		if (opts.clear) console.clear();

		Triggers.add(tmp);
		await handle();
		Triggers.delete(tmp);
	}

	let dir, output, key;
	for (dir of dirs) {
		output = await setup(dir, onChange);
		for (key in output) Watchers.set(key, output[key]);
	}

	if (opts.eager) {
		await callback();
	}
}

export async function run() {
	try {
		let pid = await toExec.apply(0, arguments);
		if (pid.stdout) process.stdout.write(pid.stdout);
		if (pid.stderr) process.stderr.write(pid.stderr);
	} catch (err) {
		console.log(`[ERROR] ${err.message}`); // TODO: beep?
		if (err.stdout) process.stdout.write(err.stdout);
		if (err.stderr) process.stderr.write(err.stderr);
	}
}