/* eslint-disable no-control-regex */
import chalk from "chalk";
import { exec } from "child_process";
import del from "del";
import { series, src, task, watch } from "gulp";
import mocha from "gulp-mocha";
import * as path from "path";

task("test", () => src("out/tests/Main.js", { read: false })
	.pipe(mocha({ reporter: "even-more-min" }))
	.on("error", () => process.exitCode = 1));

task("build", series(
	clean,
	compile,
	"test"));

task("watch", series("build", done =>
	watch("./src/**/*.ts", series("build"))
	&& done()));

async function compile () {
	await new TypescriptWatch("src", "out").once();
}

async function clean () {
	await del("out");
}

export default class TypescriptWatch {
	private onDataHandler: (data: string) => any;
	private onCompleteHandler: (done: () => any) => any;
	private initialized: (() => void) | true | undefined;
	private readonly inDir: string;
	private readonly outDir: string;
	private declaration: string | undefined;

	public constructor (dir: string, outDir: string) {
		this.inDir = path.resolve(dir);
		this.outDir = path.resolve(outDir);
	}

	public onData (handler: (data: string) => boolean | undefined | void) {
		this.onDataHandler = handler;
		return this;
	}

	public onComplete (handler: (done: () => any) => any) {
		this.onCompleteHandler = handler;
		return this;
	}

	public setDeclaration (dir: string) {
		this.declaration = path.resolve(dir);
		return this;
	}

	public async once () {
		const ocwd = process.cwd();
		process.chdir(this.inDir);
		const declaration = this.declaration ? `--declaration --declarationDir ${this.declaration}` : "";
		const task = exec(`npx tsc --outDir ${this.outDir} --pretty ${declaration}`);
		process.chdir(ocwd);

		task.stderr!.on("data", data => process.stderr.write(data));

		task.stdout!.on("data", (data: Buffer) => {
			if (this.onDataHandler && this.onDataHandler(data.toString()) === false)
				return;

			process.stdout.write(handleTscOut(0, data, `${path.relative(ocwd, this.inDir).replace(/\\/g, "/")}/`));
		});

		return new Promise<void>((resolve, reject) => task.on("close", code => {
			if (!code) resolve();
			else reject(code);
		}));
	}

	public watch () {
		const ocwd = process.cwd();
		process.chdir(this.inDir);
		const declaration = this.declaration ? `--declaration --declarationDir ${this.declaration}` : "";
		const task = exec(`npx tsc --outDir ${this.outDir} --pretty --watch ${declaration}`);
		process.chdir(ocwd);

		task.stderr!.on("data", data => process.stderr.write(data));

		let start: number;
		task.stdout!.on("data", (data: Buffer) => {
			if (this.onDataHandler && this.onDataHandler(data.toString()) === false)
				return;

			if (/\bincremental compilation|in watch mode\b/.test(data.toString()))
				start = Date.now();

			process.stdout.write(handleTscOut(start, data, `${path.relative(ocwd, this.inDir).replace(/\\/g, "/")}/`));

			if (/Watching for file changes./.test(data.toString()) && this.initialized) {
				if (typeof this.initialized === "function") {
					this.initialized();
					this.initialized = true;

				} else {
					this.onCompleteHandler(() => { return; });
				}
			}
		});

		return this;
	}

	public async waitForInitial () {
		return new Promise<void>(resolve => {
			if (this.initialized === true) return resolve();

			this.initialized = resolve;
		});
	}
}

function handleTscOut (startTime: number, data: string | Buffer, prefix?: string) {
	data = data.toString()
		.replace("\u001bc", "")
		.replace(/. Watching for file changes.\r\n/, ` after ${getTimeString(startTime)}`)
		.replace(/(incremental compilation...|in watch mode...)\r\n/g, "$1")
		.replace(/( TS\d{4}: [^\r\n]*?\r\n)\r\n/g, "$1")
		.replace(/(~+[^\r\n]*?\r\n)\r\n\r\n/g, "$1")
		.replace(/(?=\[30;47m(\d+| +)\u001b\[0m)/g, "\t");

	if (prefix) {
		data = data.replace(/(\u001b\[96m.*?\u001b\[0m:\u001b\[93m)/g, chalk.cyan(`${prefix}$1`));
	}

	return data;
}

function getTimeString (start: number) {
	const time = Date.now() - start;
	let timeString;

	if (time >= 1000) {
		timeString = `${(time / 1000).toFixed(2).replace(/0+$/, "")} s`;

	} else if (time >= 100) {
		timeString = `${(time / 100).toFixed(0)} ms`;

	} else {
		timeString = `${time} μs`;
	}

	return chalk.magenta(timeString);
}