import { RawData, WebSocket } from 'ws';
import { Main } from '../Main';
import { delay, ServiceState } from '../Utils';

const PREFIX = '[Unity]';
export class UnityWebSocket {
	private _Main: Main;

	state: ServiceState = 'DISCONNECTED';
	message?: string = 'Waiting for process...';
	error?: string;

	parameters: UnityParameters = {
		timelineWatching: false,
		timelineStanding: false,
		timelineProgress: 0,
		zedPath: '',
		zedReady: false,
		zedFPS: '-',
		parameters: [],
	};

	socket: WebSocket;

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

	handle(command: string, ...args: any[]) {
		switch (command) {
			case 'parameterValue':
				const sliderIndex: number = args[0];
				const percentage: number = args[1];

				if (this.parameters.parameters[sliderIndex] == undefined)
					return;
				this.parameters.parameters[sliderIndex].outputValue =
					percentage;

				if (
					this.socket == null ||
					this.socket.readyState !== WebSocket.OPEN
				)
					return;

				this.socket.send(
					JSON.stringify({
						type: 'set_slider_value',
						sliderIndex: sliderIndex,
						sliderValue: percentage,
					})
				);

				this.broadcastState();
				break;
		}
	}

	broadcastState() {
		this._Main.WebServer.socket.emit(
			'unityWebSocketState',
			this.getState()
		);
	}

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

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

	stopFetchClocks() {
		clearInterval(this.heartbeatClock);
		clearInterval(this.calibrationImageClock);
	}

	handleSocketMessage(data: RawData) {
		let message: UnitySocketMessage;
		try {
			message = JSON.parse(data.toString());
		} catch (error) {
			return;
		}

		switch (message.type) {
			case 'heartbeat_data':
				this.parameters.timelineWatching =
					message.heartbeat.dataTimeline.isWatching;
				this.parameters.timelineStanding =
					message.heartbeat.dataTimeline.isStanding;
				this.parameters.timelineProgress =
					message.heartbeat.dataTimeline.timelineProgress;
				this.parameters.zedPath = `${message.heartbeat.zedCamera.streamInputIP}:${message.heartbeat.zedCamera.streamInputPort}`;
				this.parameters.zedReady =
					message.heartbeat.zedCamera.isZedReady;
				this.parameters.zedFPS = message.heartbeat.zedCamera.cameraFPS;
				this.parameters.parameters = message.heartbeat.dataSliders;
				this.broadcastState();
				break;

			case 'response_camera_frame':
				this._Main.WebServer.Calibration.writeCalibrationImage(
					message.imageBase64
				);
				break;
		}
	}

	disconnected: boolean = false;
	async disconnect() {
		this.restartRequested = true;
		this.disconnected = true;

		if (this.socket != null) {
			this.socket.close();
			this.socket = null;
		}

		this.stopFetchClocks();

		this.setInfo('Waiting for process...', null, 'DISCONNECTED');
	}

	private restartRequested = false;
	async reconnect() {
		if (this.restartRequested) return;
		if (this.disconnected) return;
		this.restartRequested = true;

		this.stopFetchClocks();

		if (this.socket != null) {
			this.socket.close();
			this.socket = null;
		}

		await delay(2000);
		if (this.disconnected) return;

		this.message = `Reconnecting in 10 seconds...`;
		this.broadcastState();

		await delay(10000);
		if (this.disconnected) return;

		await this.connect();
	}

	async connect() {
		this.restartRequested = false;
		this.disconnected = false;

		this.stopFetchClocks();

		this.setInfo('Connecting...', null, 'CONNECTING');

		await delay(1000);

		this.socket = new WebSocket(
			`ws://${this._Main.Config.unity.webSocket.ip}:${this._Main.Config.unity.webSocket.port}`
		);

		this.socket.on('error', (error) => {
			if (this.restartRequested) return;
			this.setInfo(
				'Connection error',
				`Could not connect: ${error.message}`,
				'FAILED'
			);
			this.reconnect();
		});

		this.socket.on('open', () => {
			this.startFetchClocks();

			this.setInfo('Connected', null, 'CONNECTED');
		});

		this.socket.on('close', () => {
			if (this.restartRequested) return;
			this.setInfo(
				'Disconnected',
				'Unity was disconnected unexpectedly',
				'FAILED'
			);
			this.reconnect();
		});

		this.socket.on('message', (data) => this.handleSocketMessage(data));
	}

	private heartbeatClock: NodeJS.Timeout;
	private calibrationImageClock: NodeJS.Timeout;
	startFetchClocks() {
		this.socket.send(
			JSON.stringify({ type: 'set_heartbeat_auto_send', autoSend: false })
		);
		this.heartbeatClock = setInterval(() => {
			if (
				this.socket == null ||
				this.socket.readyState !== WebSocket.OPEN
			)
				return;

			this.socket.send(JSON.stringify({ type: 'request_heartbeat' }));
		}, this._Main.Config.unity.heartbeatInterval);

		this.calibrationImageClock = setInterval(() => {
			if (
				this.socket == null ||
				this.socket.readyState !== WebSocket.OPEN
			)
				return;

			this.socket.send(JSON.stringify({ type: 'request_camera_frame' }));
		}, this._Main.Config.unity.calibrationImageInterval);
	}

	getState(): UnityWebSocketStatus {
		return {
			state: this.state,
			message: this.message,
			error: this.error,

			parameters: this.parameters,
		};
	}
}

interface UnityWebSocketStatus {
	state: ServiceState;
	message?: string;
	error?: string;

	parameters: UnityParameters;
}

interface UnityParameters {
	timelineWatching: boolean;
	timelineStanding: boolean;
	timelineProgress: number;
	zedPath: string;
	zedReady: boolean;
	zedFPS: string;
	parameters: UnitySocketMessageHeartbeat['heartbeat']['dataSliders'];
}

type UnitySocketMessage =
	| UnitySocketMessageHeartbeat
	| UnitySocketMessageCameraFrame;

interface UnitySocketMessageBase {
	type: string;
	timestamp: number;
}
interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
	type: 'heartbeat_data';
	heartbeat: {
		dataSliders: {
			sliderIndex: number;
			sliderName: string;
			outputValue: number;
		}[];
		dataTimeline: {
			isStanding: boolean;
			isWatching: boolean;
			timelineProgress: number;
		};
		zedCamera: {
			cameraFPS: string;
			isZedReady: boolean;
			streamInputIP: string;
			streamInputPort: number;
			zedGrabError: number;
		};
	};
}
interface UnitySocketMessageCameraFrame extends UnitySocketMessageBase {
	type: 'response_camera_frame';
	imageBase64: string;
}
