import { pathExistsSync } from 'fs-extra';
import { ChildProcess, exec, spawn } from 'child_process';
import { delay } from '../Utils';
import { Main } from '../Main';

const PREFIX = '[UnityRunner]';

const LOG_OUTPUT = !process.argv.includes('--no-output-log');

export class UnityRunner {
	private _Main: Main;

	state: UnityRunnerState;
	message?: string = 'Awaiting startup delay...';
	error?: string;

	startTime: number = null;
	output: { current: string[]; last: string[] } = { current: [], last: [] };

	process: ChildProcess;

	constructor(Main: Main) {
		this._Main = Main;
	}

	handle(command: string, ...args: any[]) {
		switch (command) {
			case 'restart':
				const callback: (response: {
					succeed: boolean;
					message?: string;
				}) => void = args[0];
				if (typeof callback !== 'function') return;

				callback(this.requestRestart());
				break;
		}
	}

	private statusClock: NodeJS.Timeout;
	startStatusClock() {
		clearInterval(this.statusClock);
		this.statusClock = setInterval(() => {
			this.broadcastState();
		}, 3000);
	}

	requestRestart(): { succeed: boolean; message?: string } {
		if (this.state !== 'RUNNING')
			return {
				succeed: false,
				message:
					'Cannot restart when process is not running. It is probably restarting already.',
			};

		this.restart(true);
		return { succeed: true };
	}

	broadcastState() {
		this._Main.WebServer.socket.emit('unityRunnerState', this.getStatus());
	}

	setInfo(
		message: string,
		error: string,
		state: UnityRunnerState = 'PROBLEM'
	) {
		this.message = message;
		this.error = error;
		this.state = state;
		this.broadcastState();

		this.output.current.push(
			`[${new Date().toLocaleTimeString('nl-NL')}] [System] [${state}] ${
				message ?? error
			}`
		);

		if (state == 'PROBLEM' || state == 'STOPPED')
			console.warn(PREFIX, message ?? error);
		else console.log(PREFIX, message ?? error);
	}

	private restartTriggered: boolean = false;
	async restart(instant: boolean = false) {
		if (this.restartTriggered) return;
		clearInterval(this.statusClock);

		this._Main.WebServer.Calibration.hasCalibrationImage = false;

		this._Main.UnityWebSocket.disconnect();

		this.restartTriggered = true;
		this.startTime = -1;
		this.broadcastState();

		await delay(2000);

		if (this.output.current.length > 0) {
			this.output.last = [...this.output.current];
			this.output.current = [];
		}

		if (instant)
			this.setInfo('Process will restart shortly...', null, 'STOPPED');

		if (this.process != null) {
			this.process.kill('SIGTERM');

			await delay(3000);

			if (!this.process.killed && this.process.exitCode === null) {
				this.process.kill('SIGKILL');
				console.log(PREFIX, 'Sent SIGKILL to process.');
			}
		}
		this.startTime = -1;

		if (!instant) {
			this.message = 'Reconnecting in 10 seconds...';
			this.broadcastState();

			await delay(10000);
		}
		await this.start();
	}

	async start() {
		if (this.output.current.length > 0) {
			this.output.last = [...this.output.current];
			this.output.current = [];
		}
		this.startTime = Date.now();
		this.restartTriggered = false;
		this.broadcastState();

		this._Main.WebServer.Calibration.hasCalibrationImage = false;

		clearInterval(this.statusClock);

		const path = this._Main.Config.unity.executable.path;
		if (path == null || !pathExistsSync(path)) {
			this.setInfo(
				'Executable problem',
				`Executable path is not set or does not exist: ${path}`
			);
			return;
		}

		if (this._Main.CameraRunner.state !== 'CONNECTED')
			await new Promise<void>((resolve) => {
				this.setInfo(
					'Waiting for CameraRunner to connect...',
					null,
					'STARTING'
				);
				var c = setInterval(() => {
					if (this._Main.CameraRunner.state !== 'CONNECTED') return;

					clearInterval(c);
					resolve();
				}, 1000);
			});

		if (this._Main.CameraRunner.processStatus?.state !== 'RUNNING')
			await new Promise<void>((resolve) => {
				this.setInfo(
					'Waiting for CameraRunner process to start...',
					null,
					'STARTING'
				);
				var c = setInterval(() => {
					if (
						this._Main.CameraRunner.processStatus?.state !==
						'RUNNING'
					)
						return;

					clearInterval(c);
					resolve();
				}, 1000);
			});

		const fileName = path.split(/(\/|\\)/).pop();

		this.setInfo(`Starting executable: ${fileName}`, null, 'STARTING');

		this.process = spawn(
			path,
			this._Main.Config.unity.executable.arguments,
			{
				stdio: 'pipe',
			}
		);

		this.process.on('exit', (code, signal) => {
			if (this.restartTriggered) return;
			this.setInfo(
				'Process exited',
				`Process exited with code ${code} and signal ${signal}`,
				'STOPPED'
			);
			this.restart();
		});
		this.process.on('error', (err) => {
			this.setInfo('Process error', err.message);
			this.restart();
		});
		this.process.stdout?.on('data', (data) => {
			const lines = data
				.toString()
				.trim()
				.split('\n')
				.filter((line) => line.length > 0);
			lines.forEach((line) => {
				const formattedLine = `[${new Date().toLocaleTimeString(
					'nl-NL'
				)}] [${fileName}] ${line}`;
				if (LOG_OUTPUT) console.log(PREFIX, formattedLine);
				this.output.current.push(formattedLine);
			});
		});
		this.process.stderr?.on('data', (data) => {
			const lines = data
				.toString()
				.trim()
				.split('\n')
				.filter((line) => line.length > 0);
			lines.forEach((line) => {
				const formattedLine = `[${new Date().toLocaleTimeString(
					'nl-NL'
				)}] [${fileName}] [ERROR] ${line}`;
				if (LOG_OUTPUT) console.error(PREFIX, formattedLine);
				this.output.current.push(formattedLine);
			});
		});
		this.startStatusClock();
		setTimeout(() => {
			if (
				this.process == null ||
				this.process?.killed ||
				this.process?.exitCode != null ||
				this.restartTriggered
			)
				return;

			this.setInfo('Running', '', 'RUNNING');
			this._Main.UnityWebSocket.connect();
		}, 5000);
	}

	getStatus(): UnityRunnerStatus {
		return {
			state: this.state,
			message: this.message,
			error: this.error,

			startTime: this.startTime,
			output: this.output,
		};
	}
}

interface UnityRunnerStatus {
	state: 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
	message?: string;
	error?: string;

	startTime: number;
	output: { current: string[]; last: string[] };
}

export type UnityRunnerState = 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
