import { ce, MorphComponent, MorphFeature } from 'morphux';
import { Main } from './main';
import {
	createProgress,
	formatUptime,
	ServiceState,
	setProgressState,
	setStatusState,
	StatusType,
} from './utils';

export class DashboardUnity {
	private _Main: Main;

	container: HTMLDivElement = document.querySelector('.ntsh_dashboard-unity');

	processStatus: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-processstatus'
	);
	processInfo: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-processinfo'
	);
	restartButton: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-restart'
	);
	startButton: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-start'
	);
	stopButton: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-stop'
	);

	uptimeInfo: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-uptime'
	);

	webSocketStatus: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-websocketstatus'
	);
	webSocketInfo: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-websocketinfo'
	);

	outOfServiceStatus: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-outofservicestatus'
	);
	outOfServiceInfo: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-outofserviceinfo'
	);
	enableOutOfServiceButton: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-enableoutofservice'
	);
	disableOutOfServiceButton: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-disableoutofservice'
	);

	zedStreamStatus: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-zedstreamstatus'
	);
	zedStreamInfo: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-zedstreaminfo'
	);

	zedStreamFps: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-zedstreamfps'
	);
	zedStreamPath: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-zedstreampath'
	);

	timelineWatching: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-timeline-watching'
	);
	timelineStanding: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-timeline-standing'
	);
	timelineProgress: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-timeline-progress'
	);

	parametersTable: HTMLTableElement = document.querySelector(
		'.ntsh_dashboard-unity-parameters'
	);
	advancedParametersTable: HTMLTableElement = document.querySelector(
		'.ntsh_dashboard-unity-advancedparameters'
	);

	sensorsTable: HTMLTableElement = document.querySelector(
		'.ntsh_dashboard-unity-sensors'
	);

	errorContainer: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-error'
	);
	errorText: HTMLDivElement = document.querySelector(
		'.ntsh_dashboard-unity-errortext'
	);

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

		this.registerListeners();
	}

	private runnerError: string;
	updateRunnerState(state: UnityRunnerStatus) {
		// ----------- Process -----------
		if (state.state != 'RUNNING') {
			state.startTime = -1;

			this.restartButton.style.display = 'none';
			this.stopButton.style.display = 'none';
		} else {
			this.restartButton.style.display = 'flex';
			this.stopButton.style.display = 'flex';
		}
		this.startButton.style.display =
			state.state == 'STOPPED' ? 'flex' : 'none';

		setStatusState(
			this.processStatus,
			{
				RUNNING: 'green',
				STOPPED: 'gray',
				STARTING: 'yellow',
				PROBLEM: 'red',
			}[state.state] as StatusType
		);
		this.processInfo.innerText = state.message ?? '';

		// ----------- Uptime -----------
		const uptimeSeconds =
			state.startTime == -1 ? -1 : (Date.now() - state.startTime) / 1000;
		this.uptimeInfo.innerText = formatUptime(uptimeSeconds);

		// ----------- Error -----------
		if ((state?.error ?? '').trim().length > 0)
			this.runnerError = state.error;
		else this.runnerError = null;
		this.updateError();

		this._Main.Logs.setUnityLogs(state.output.current);
	}

	private webSocketError: string;
	updateWebSocketState(state: UnityWebSocketStatus) {
		// ----------- WebSocket -----------
		setStatusState(
			this.webSocketStatus,
			{
				CONNECTING: 'yellow',
				CONNECTED: 'green',
				DISCONNECTED: 'gray',
				FAILED: 'red',
			}[state.state] as StatusType
		);
		this.webSocketInfo.innerText = state.message ?? '';

		// ----------- Out of Service -----------
		setStatusState(
			this.outOfServiceStatus,
			state.parameters.outOfService == null
				? 'gray'
				: state.parameters.outOfService
				? 'red'
				: 'green'
		);
		this.outOfServiceInfo.innerText =
			state.parameters.outOfService == null
				? ''
				: state.parameters.outOfService
				? 'Out of Service'
				: 'Operational';

		this.enableOutOfServiceButton.style.display =
			state.parameters.outOfService == null ||
			state.parameters.outOfService
				? 'none'
				: 'flex';
		this.disableOutOfServiceButton.style.display =
			state.parameters.outOfService &&
			state.parameters.outOfService != null
				? 'flex'
				: 'none';

		// ----------- ZED Stream -----------
		setStatusState(
			this.zedStreamStatus,
			state.parameters.zedReady ? 'green' : 'red'
		);
		this.zedStreamInfo.innerText = state.parameters.zedReady
			? `Connected to ${state.parameters.zedPath}`
			: 'Not ready';
		this.zedStreamFps.innerText =
			state.parameters.zedFPS == '-' ? '' : state.parameters.zedFPS;

		// ----------- Timeline -----------
		this.timelineWatching.innerText = state.parameters.timelineWatching
			? 'Yes'
			: 'No';
		this.timelineStanding.innerText = state.parameters.timelineStanding
			? 'Yes'
			: 'No';
		setProgressState(
			this.timelineProgress,
			Math.round(state.parameters.timelineProgress * 100),
			0,
			100,
			'%'
		);

		// ----------- Parameters -----------
		this.renderParameterSliders(
			state.state == 'CONNECTED' ? state.parameters.sliders : []
		);
		this.renderAdvancedParameterSliders(
			state.state == 'CONNECTED' ? state.parameters.advancedSliders : []
		);
		this.renderParameterSensors(
			state.state == 'CONNECTED' ? state.parameters.sensors : []
		);

		// ----------- Error -----------
		if ((state?.error ?? '').trim().length > 0)
			this.webSocketError = state.error;
		else this.webSocketError = null;
		this.updateError();
	}

	private renderParameterSliders(sliders: UnityParameters['sliders']) {
		const existingSliders = this.parametersTable.querySelectorAll(
			'.ntsh_dashboard-unity-parameter-row'
		);

		if (existingSliders.length !== sliders.length) {
			this.parametersTable.innerHTML = '';

			if (sliders.length === 0) {
				const row = ce('tr');
				const cell = ce('td');
				cell.appendChild(
					ce(
						'div',
						['mux_text', 'ntsh_dashboard-unity-parameters-loading'],
						null,
						'Waiting for Unity...'
					)
				);
				row.appendChild(cell);
				this.parametersTable.appendChild(row);
			} else
				sliders.forEach((slider) => {
					const multiplierFactor = slider.visualMultiplier ?? 1;
					const decimalPlacesFactor =
						10 ** (slider.decimalPlaces ?? 0);

					const value =
						Math.round(
							slider.outputValue *
								multiplierFactor *
								decimalPlacesFactor
						) / decimalPlacesFactor;

					const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');

					const nameCell = ce('td');
					nameCell.appendChild(
						ce('div', 'mux_text', null, slider.sliderName)
					);
					row.appendChild(nameCell);

					const progressCell = ce('td', 'no-service');
					progressCell.appendChild(
						createProgress(
							value,
							slider.min * multiplierFactor,
							slider.max * multiplierFactor,
							slider.unit
						)
					);
					row.appendChild(progressCell);

					const sliderCell = ce('td', 'only-service');
					const sliderProgress = createProgress(
						value,
						slider.min * multiplierFactor,
						slider.max * multiplierFactor,
						slider.unit
					);
					const sliderValue: HTMLDivElement =
						sliderProgress.querySelector('.ntsh_progress-value');
					sliderValue.classList.add('mux_resizer');
					sliderCell.appendChild(sliderProgress);

					const resizer = new MorphComponent.Resizer({
						existingContainer: sliderValue,
						direction: 'right',
						relative: true,
						min: 0,
						max: () => sliderProgress.clientWidth,
					});
					let lastValue: number = -1;
					resizer.on('resized', (size) => {
						const percentage =
							Math.round(
								(size / sliderProgress.clientWidth) * 100
							) / 100;

						var actualValue =
							slider.min + percentage * (slider.max - slider.min);

						if (actualValue === lastValue) return;
						lastValue = actualValue;

						this._Main.socket.emit(
							'unityWebSocket',
							'parameterValue',
							slider.sliderIndex,
							actualValue
						);
						setProgressState(
							sliderProgress,
							Math.round(
								actualValue *
									multiplierFactor *
									decimalPlacesFactor
							) / decimalPlacesFactor,
							slider.min * multiplierFactor,
							slider.max * multiplierFactor,
							slider.unit
						);
					});

					row.appendChild(sliderCell);

					this.parametersTable.appendChild(row);
				});
		} else {
			existingSliders.forEach((row, index) => {
				const slider = sliders[index];
				const multiplierFactor = slider.visualMultiplier ?? 1;
				const decimalPlacesFactor = 10 ** (slider.decimalPlaces ?? 0);

				const value =
					Math.round(
						slider.outputValue *
							multiplierFactor *
							decimalPlacesFactor
					) / decimalPlacesFactor;

				const progressElement: HTMLDivElement = row.querySelector(
					'.no-service .ntsh_progress'
				);
				setProgressState(
					progressElement,
					value,
					slider.min * multiplierFactor,
					slider.max * multiplierFactor,
					slider.unit
				);

				const sliderElement: HTMLDivElement = row.querySelector(
					'.only-service .ntsh_progress'
				);
				if (sliderElement.querySelector('.mux_resizer-moving') == null)
					setProgressState(
						sliderElement,
						value,
						slider.min * multiplierFactor,
						slider.max * multiplierFactor,
						slider.unit
					);
			});
		}
	}

	private renderAdvancedParameterSliders(
		sliders: UnityParameters['sliders']
	) {
		const existingSliders = this.advancedParametersTable.querySelectorAll(
			'.ntsh_dashboard-unity-parameter-row'
		);

		if (existingSliders.length !== sliders.length) {
			this.advancedParametersTable.innerHTML = '';

			if (sliders.length === 0) {
				const row = ce('tr');
				const cell = ce('td');
				cell.appendChild(
					ce(
						'div',
						['mux_text', 'ntsh_dashboard-unity-parameters-loading'],
						null,
						'Waiting for Unity...'
					)
				);
				row.appendChild(cell);
				this.advancedParametersTable.appendChild(row);
			} else
				sliders.forEach((slider) => {
					const multiplierFactor = slider.visualMultiplier ?? 1;
					const decimalPlacesFactor =
						10 ** (slider.decimalPlaces ?? 0);

					const value =
						Math.round(
							slider.outputValue *
								multiplierFactor *
								decimalPlacesFactor
						) / decimalPlacesFactor;

					const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');

					const nameCell = ce('td');
					nameCell.appendChild(
						ce('div', 'mux_text', null, slider.sliderName)
					);
					row.appendChild(nameCell);

					const progressCell = ce('td', 'no-service');
					progressCell.appendChild(
						createProgress(
							value,
							slider.min * multiplierFactor,
							slider.max * multiplierFactor,
							slider.unit
						)
					);
					row.appendChild(progressCell);

					const sliderCell = ce('td', 'only-service');
					const sliderProgress = createProgress(
						value,
						slider.min * multiplierFactor,
						slider.max * multiplierFactor,
						slider.unit
					);
					const sliderValue: HTMLDivElement =
						sliderProgress.querySelector('.ntsh_progress-value');
					sliderValue.classList.add('mux_resizer');
					sliderCell.appendChild(sliderProgress);

					const resizer = new MorphComponent.Resizer({
						existingContainer: sliderValue,
						direction: 'right',
						relative: true,
						min: 0,
						max: () => sliderProgress.clientWidth,
					});
					let lastValue: number = -1;
					resizer.on('resized', (size) => {
						const percentage =
							Math.round(
								(size / sliderProgress.clientWidth) * 100
							) / 100;

						var actualValue =
							slider.min + percentage * (slider.max - slider.min);

						if (actualValue === lastValue) return;
						lastValue = actualValue;

						this._Main.socket.emit(
							'unityWebSocket',
							'advancedParameterValue',
							slider.sliderIndex,
							actualValue
						);
						setProgressState(
							sliderProgress,
							Math.round(
								actualValue *
									multiplierFactor *
									decimalPlacesFactor
							) / decimalPlacesFactor,
							slider.min * multiplierFactor,
							slider.max * multiplierFactor,
							slider.unit
						);
					});

					row.appendChild(sliderCell);

					this.advancedParametersTable.appendChild(row);
				});
		} else {
			existingSliders.forEach((row, index) => {
				const slider = sliders[index];
				const multiplierFactor = slider.visualMultiplier ?? 1;
				const decimalPlacesFactor = 10 ** (slider.decimalPlaces ?? 0);

				const value =
					Math.round(
						slider.outputValue *
							multiplierFactor *
							decimalPlacesFactor
					) / decimalPlacesFactor;

				const progressElement: HTMLDivElement = row.querySelector(
					'.no-service .ntsh_progress'
				);
				setProgressState(
					progressElement,
					value,
					slider.min * multiplierFactor,
					slider.max * multiplierFactor,
					slider.unit
				);

				const sliderElement: HTMLDivElement = row.querySelector(
					'.only-service .ntsh_progress'
				);
				if (sliderElement.querySelector('.mux_resizer-moving') == null)
					setProgressState(
						sliderElement,
						value,
						slider.min * multiplierFactor,
						slider.max * multiplierFactor,
						slider.unit
					);
			});
		}
	}

	private renderParameterSensors(sensors: UnityParameters['sensors']) {
		const existingSensors = this.sensorsTable.querySelectorAll(
			'.ntsh_dashboard-unity-sensor-row'
		);

		if (existingSensors.length !== sensors.length) {
			this.sensorsTable.innerHTML = '';

			if (sensors.length === 0) {
				const row = ce('tr');
				const cell = ce('td');
				cell.appendChild(
					ce(
						'div',
						['mux_text', 'ntsh_dashboard-unity-sensors-loading'],
						null,
						'Waiting for Unity...'
					)
				);
				row.appendChild(cell);
				this.sensorsTable.appendChild(row);
			} else
				sensors.forEach((sensor) => {
					const row = ce('tr', 'ntsh_dashboard-unity-sensor-row');

					const nameCell = ce('td');
					nameCell.appendChild(
						ce('div', 'mux_text', null, sensor.deviceName)
					);
					row.appendChild(nameCell);

					const progressCell = ce('td');
					progressCell.appendChild(
						createProgress(
							Math.round(sensor.outputValue * 100),
							0,
							100,
							'%'
						)
					);
					row.appendChild(progressCell);

					this.sensorsTable.appendChild(row);
				});
		} else {
			existingSensors.forEach((row, index) => {
				const value = sensors[index].outputValue;

				const progressElement: HTMLDivElement =
					row.querySelector('.ntsh_progress');
				setProgressState(
					progressElement,
					Math.round(value * 100),
					0,
					100,
					'%'
				);
			});
		}
	}

	private updateError() {
		const errors: string[] = [];
		if (this.runnerError != null) errors.push(this.runnerError);
		if (this.webSocketError != null) errors.push(this.webSocketError);

		if (errors.length > 0) {
			this.errorText.innerText = errors.join('\n');
			this.errorContainer.style.display = 'block';
		} else {
			this.errorContainer.style.display = 'none';
			this.errorText.innerText = '';
		}
	}

	registerListeners() {
		this._Main.socket.on('unityRunnerState', (state: UnityRunnerStatus) =>
			this.updateRunnerState(state)
		);
		this._Main.socket.on(
			'unityWebSocketState',
			(state: UnityWebSocketStatus) => this.updateWebSocketState(state)
		);

		this.restartButton.addEventListener('click', async () => {
			this.executeCommand(
				'restart',
				'Are you sure you want to restart the Unity Runner process?'
			);
		});
		this.startButton.addEventListener('click', async () => {
			this.executeCommand(
				'start',
				'Are you sure you want to start the Unity Runner process?'
			);
		});
		this.stopButton.addEventListener('click', async () => {
			this.executeCommand(
				'stop',
				'Are you sure you want to stop the Unity Runner process?'
			);
		});

		this.enableOutOfServiceButton.addEventListener('click', async () => {
			this.executeCommand(
				'enableOutOfService',
				'Are you sure you want to set the installation to "Out of Service"?',
				'unityWebSocket'
			);
		});

		this.disableOutOfServiceButton.addEventListener('click', async () => {
			this.executeCommand(
				'disableOutOfService',
				'Are you sure you want to set the installation to "Operational"?',
				'unityWebSocket'
			);
		});
	}

	private async executeCommand(
		command: string,
		message: string,
		type: 'unityRunner' | 'unityWebSocket' = 'unityRunner'
	) {
		const confirmed = await MorphFeature.Confirm({
			title: 'Are you sure?',
			message,
		});
		if (!confirmed) return;

		MorphFeature.Loader({
			active: true,
			message: `Dispatching command...`,
		});
		this._Main.socket.emit(
			type,
			command,
			(response: { succeed: boolean; message?: string }) => {
				MorphFeature.Loader({ active: false });

				if (!response.succeed)
					return MorphFeature.Alert({
						title: 'Error',
						message: response.message,
					});

				MorphFeature.Notification({
					level: 'success',
					message: `Dispatched command`,
				});
			}
		);
	}
}

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

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

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

	parameters: UnityParameters;
}

interface UnityParameters {
	timelineWatching: boolean;
	timelineStanding: boolean;
	timelineProgress: number;
	zedPath: string;
	zedReady: boolean;
	zedFPS: string;
	outOfService: boolean;
	sliders: UnityParameterSlider[];
	advancedSliders: UnityParameterSlider[];
	sensors: UnitySocketMessageHeartbeat['heartbeat']['dataSensors'];
}

type UnityHeartbeatSlider =
	UnitySocketMessageHeartbeat['heartbeat']['dataSliders'][number];
interface UnityParameterSlider extends UnityHeartbeatSlider {
	visualMultiplier: number;
	decimalPlaces: number;
}

interface UnitySocketMessageBase {
	type: string;
	timestamp: number;
}
interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
	type: 'heartbeat_data';
	heartbeat: {
		dataSensors: {
			sensorIndex: number;
			deviceName: string;
			outputValue: number;
		}[];
		dataSliders: {
			sliderIndex: number;
			sliderName: string;
			outputValue: number;
			min: number;
			max: number;
			unit: string;
		}[];
		dataTimeline: {
			isStanding: boolean;
			isWatching: boolean;
			timelineProgress: number;
		};
		zedCamera: {
			cameraFPS: string;
			isZedReady: boolean;
			streamInputIP: string;
			streamInputPort: number;
			zedGrabError: number;
		};
		showOutOfService?: boolean;
	};
}
