/*****************************************************************************
#                                                                            #
#    KVMD - The main PiKVM daemon.                                           #
#                                                                            #
#    Copyright (C) 2018-2024  Maxim Devaev <mdevaev@gmail.com>               #
#                                                                            #
#    This program is free software: you can redistribute it and/or modify    #
#    it under the terms of the GNU General Public License as published by    #
#    the Free Software Foundation, either version 3 of the License, or       #
#    (at your option) any later version.                                     #
#                                                                            #
#    This program is distributed in the hope that it will be useful,         #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of          #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           #
#    GNU General Public License for more details.                            #
#                                                                            #
#    You should have received a copy of the GNU General Public License       #
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.  #
#                                                                            #
*****************************************************************************/


"use strict";


import {tools, $} from "../tools.js";
import {wm} from "../wm.js";

import {JanusStreamer} from "./stream_janus.js";
import {MediaStreamer} from "./stream_media.js";
import {MjpegStreamer} from "./stream_mjpeg.js";


export function Streamer() {
	var self = this;

	/************************************************************************/

	var __janus_imported = null;
	var __streamer = null;

	var __state = null;
	var __res = {"width": 640, "height": 480};

	var __init__ = function() {
		__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo, __watchHook, __organizeHook);

		$("stream-led").title = "No stream from PiKVM";

		tools.slider.setParams($("stream-quality-slider"), 5, 100, 5, 80, function(value) {
			$("stream-quality-value").innerText = `${value}%`;
		});
		$("stream-quality-slider").onchange = ((ev) => __sendParam("quality", ev.target.valueAsNumber));

		tools.slider.setParams($("stream-h264-bitrate-slider"), 25, 20000, 25, 5000, function(value) {
			$("stream-h264-bitrate-value").innerText = value;
		});
		$("stream-h264-bitrate-slider").onchange = ((ev) => __sendParam("h264_bitrate", ev.target.valueAsNumber));

		tools.slider.setParams($("stream-h264-gop-slider"), 0, 60, 1, 30, function(value) {
			$("stream-h264-gop-value").innerText = value;
		});
		$("stream-h264-gop-slider").onchange = ((ev) => __sendParam("h264_gop", ev.target.valueAsNumber));

		tools.slider.setParams($("stream-desired-fps-slider"), 0, 120, 1, 0, function(value) {
			$("stream-desired-fps-value").innerText = (value === 0 ? "Unlimited" : value);
		});
		$("stream-desired-fps-slider").onchange = ((ev) => __sendParam("desired_fps", ev.target.valueAsNumber));

		$("stream-resolution-selector").onchange = ((ev) => __sendParam("resolution", ev.target.value));

		tools.radio.setOnClick("stream-mode-radio", __clickModeRadio, false);

		// Not getInt() because of radio is a string container.
		// Also don't reset Streamer at class init.
		tools.radio.clickValue("stream-orient-radio", tools.storage.get("stream.orient", 0));
		tools.radio.setOnClick("stream-orient-radio", function() {
			let orient = parseInt(tools.radio.getValue("stream-orient-radio"));
			tools.storage.setInt("stream.orient", orient);
			__streamer.setOrientation(orient);
		}, false);

		tools.el.setOnClick($("stream-multimedia-switch"), function() {
			let enabled = $("stream-multimedia-switch").checked;
			tools.el.setEnabled($("stream-audio-volume-slider"), enabled);
			tools.el.setEnabled($("stream-mic-switch"), enabled);
			tools.el.setEnabled($("stream-mic-raw-switch"), enabled);
			tools.el.setEnabled($("stream-camera-switch"), enabled);
			__applyAudioVolume();
			__applyMicEnabled();
			__applyCameraEnabled();
			tools.storage.setBool("stream.multimedia", enabled);
		}, false);

		tools.storage.bindSimpleSlider($("stream-audio-volume-slider"), "stream.audio", 0, 100, 1, 100, __applyAudioVolume);

		tools.storage.bindSimpleSwitch($("stream-mic-raw-switch"), "stream.mic.raw", false, __applyMicEnabled);

		for (let [input, apply_cb] of [["mic", __applyMicEnabled], ["camera", __applyCameraEnabled]]) {
			tools.storage.bindSimpleSwitch($(`stream-${input}-switch`), `stream.${input}`, false, apply_cb);

			let el = $(`stream-${input}-selector`);
			tools.selector.addOption(el, `\u2500 Default ${input} \u2500`, ".__default__");

			let id = tools.storage.get(`stream.${input}.device.id`, ".__default__");
			if (id !== ".__default__") {
				let name = tools.storage.get(`stream.${input}.device.name`, "???");
				tools.selector.addOption(el, name, id, true);
			}

			el.onchange = apply_cb;
		}

		tools.el.setOnClick($("stream-screenshot-button"), __clickScreenshotButton);
		tools.el.setOnClick($("stream-reset-button"), __clickResetButton);

		tools.storage.bindSimpleSwitch($("stream-suspend-switch"), "stream.suspend", false, __visibilityHook);

		$("stream-window").show_hook = __visibilityHook;
		$("stream-window").close_hook = __visibilityHook;
		$("stream-window").organize_hook = __organizeHook;

		document.addEventListener("visibilitychange", __visibilityHook);
	};

	/************************************************************************/

	var __applyAudioVolume = function() {
		let volume = $("stream-audio-volume-slider").valueAsNumber;
		$("stream-audio-volume-value").innerText = volume + "%";
		let mm = $("stream-multimedia-switch").checked;
		__streamer.setAudioVolume(mm ? volume : 0);
	};

	var __applyMicEnabled = function() {
		let enabled = ($("stream-multimedia-switch").checked && $("stream-mic-switch").checked);
		let el = $("stream-mic-selector");
		tools.el.setEnabled(el, enabled);
		__streamer.setMicRaw($("stream-mic-raw-switch").checked);
		__streamer.setMicDevice(enabled ? el.value : null);
	};

	var __applyCameraEnabled = function() {
		let enabled = ($("stream-multimedia-switch").checked && $("stream-camera-switch").checked);
		let el = $("stream-camera-selector");
		tools.el.setEnabled(el, enabled);
		__streamer.setCameraDevice(enabled ? el.value : null);
	};

	var __isStreamRequired = function() {
		return (
			tools.hidden.isVisible($("stream-window"))
			&& (
				!$("stream-suspend-switch").checked
				|| (document.visibilityState === "visible")
			)
		);
	};

	var __visibilityHook = function() {
		let req = __isStreamRequired();
		__applyState(req ? __state : null);
	};

	var __organizeHook = function() {
		let geo = self.getGeometry();
		wm.setAspectRatio($("stream-window"), geo.width, geo.height);
	};

	var __first_watch = true;
	var __watchHook = function() {
		if (__first_watch) {
			__first_watch = false;
			let el = $("stream-multimedia-switch");
			if (!el.checked && tools.storage.getBool("stream.multimedia")) {
				let text = (
					"In the previous session, you used the multimedia features of PiKVM.<br>"
					+ "Do you want to continue with the same settings?<br>"
					+ "<br>"
					+ "Due to browser limitations, this requires explicit confirmation."
					+ "<ul><li><b>OK</b> - continue with multimedia.</li><li><b>Cancel</b> - forget about it.</li></ul>"
				);
				wm.confirm(text).then(function(ok) {
					if (ok) {
						if (!el.checked) {
							el.click();
						}
						setTimeout(function() {
							__applyAudioVolume();
							__applyMicEnabled();
							__applyCameraEnabled();
						}, 100);
					} else {
						tools.storage.setBool("stream.multimedia", false);
					}
				});
			}
		}
	};

	self.ensureDeps = function(cb) {
		JanusStreamer.ensure_janus(function(avail) {
			__janus_imported = avail;
			cb();
		});
	};

	self.getGeometry = function() {
		// Первоначально обновление геометрии считалось через ResizeObserver.
		// Но оно не ловило некоторые события, например в последовательности:
		//   - Находять в HD переходим в фулскрин
		//   - Меняем разрешение на маленькое
		//   - Убираем фулскрин
		//   - Переходим в HD
		//   - Видим нарушение пропорций
		// Так что теперь используются быстре рассчеты через offset*
		// вместо getBoundingClientRect().
		let res = __streamer.getResolution();
		let ratio = Math.min(res.view_width / res.real_width, res.view_height / res.real_height);
		return {
			"x": Math.round((res.view_width - ratio * res.real_width) / 2),
			"y": Math.round((res.view_height - ratio * res.real_height) / 2),
			"width": Math.round(ratio * res.real_width),
			"height": Math.round(ratio * res.real_height),
			"real_width": res.real_width,
			"real_height": res.real_height,
		};
	};

	self.setState = function(state) {
		if (state) {
			if (!__state) {
				__state = {};
			}
			if (state.features !== undefined) {
				__state.features = state.features;
				__state.limits = state.limits; // Following together with features
			}
			if (__state.features !== undefined && state.streamer !== undefined) {
				__state.streamer = state.streamer;
				__setControlsEnabled(!!state.streamer);
			}
		} else {
			__state = null;
			__setControlsEnabled(false);
		}
		__applyState((__isStreamRequired() && __state && __state.features) ? state : null);
	};

	var __applyState = function(state) {
		if (__janus_imported === null) {
			// XXX: This warning is triggered by visibilitychange event via the __visibilityHook()
			// alert("__janus_imported is null, please report");
			return;
		}

		if (!state) {
			__streamer.stopStream();
			return;
		}

		if (state.features) {
			let f = state.features;
			let l = state.limits;
			let sup_h264 = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
			let sup_vd = MediaStreamer.is_videodecoder_available();
			let sup_webrtc = JanusStreamer.is_webrtc_available();
			let has_media = (f.h264 && sup_vd); // Don't check sup_h264 for sure
			let has_janus = (__janus_imported && f.h264 && sup_webrtc); // Same

			tools.info(
				`Stream: Janus WebRTC state: features.h264=${f.h264},`
				+ ` webrtc=${sup_webrtc}, h264=${sup_h264}, janus_imported=${__janus_imported}`);

			tools.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !sup_webrtc);
			tools.hidden.setVisible($("stream-message-no-vd"), f.h264 && !sup_vd);
			tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !sup_h264);

			tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
			if (f.resolution) {
				let el = $("stream-resolution-selector");
				el.options.length = 0;
				for (let res of l.available_resolutions) {
					tools.selector.addOption(el, res, res);
				}
			} else {
				$("stream-resolution-selector").options.length = 0;
			}
			if (f.h264) {
				tools.slider.setRange($("stream-h264-bitrate-slider"), l.h264_bitrate.min, l.h264_bitrate.max);
				tools.slider.setRange($("stream-h264-gop-slider"), l.h264_gop.min, l.h264_gop.max);
			}

			tools.feature.setEnabled($("stream-quality"), f.quality);
			tools.feature.setEnabled($("stream-resolution"), f.resolution);
			tools.feature.setEnabled($("stream-h264-bitrate"), f.h264);
			tools.feature.setEnabled($("stream-h264-gop"), f.h264);
			tools.feature.setEnabled($("stream-mode"), f.h264);

			let mode = tools.storage.get("stream.mode", "janus");
			if (mode === "janus" && !has_janus) {
				mode = "media";
			}
			if (mode === "media" && !has_media) {
				mode = "mjpeg";
			}
			tools.radio.clickValue("stream-mode-radio", mode);
		}

		if (state.applied) {
			let a = state.applied;
			if (a.resolution !== undefined) {
				let el = $("stream-resolution-selector");
				if (!tools.selector.hasValue(el, a.resolution)) {
					tools.selector.addOption(el, a.resolution, a.resolution);
				}
				el.value = a.resolution;
			}
			if (a.quality !== undefined) {
				tools.slider.setValue($("stream-quality-slider"), Math.max(a.quality, 1));
			}
			if (a.desired_fps !== undefined) {
				tools.slider.setValue($("stream-desired-fps-slider"), a.desired_fps);
			}
			if (a.h264_bitrate !== undefined) {
				tools.slider.setValue($("stream-h264-bitrate-slider"), a.h264_bitrate);
				tools.slider.setValue($("stream-h264-gop-slider"), a.h264_gop); // Following together with bitrate
			}
		}

		if (state.streamer) {
			let s = state.streamer;
			__res = s.source.resolution;
			__streamer.ensureStream(s);
			//__applyAudioVolume();
			//__applyMicEnabled();
			//__applyCameraEnabled();
		}
	};

	var __setActive = function() {
		$("stream-led").className = "led-green";
		$("stream-led").title = "Stream is active";
	};

	var __setInactive = function() {
		$("stream-led").className = "led-gray";
		$("stream-led").title = "No stream from PiKVM";
	};

	var __setControlsEnabled = function(enabled) {
		tools.el.setEnabled($("stream-quality-slider"), enabled);
		tools.el.setEnabled($("stream-desired-fps-slider"), enabled);
		tools.el.setEnabled($("stream-resolution-selector"), enabled);
		tools.el.setEnabled($("stream-h264-bitrate-slider"), enabled);
		tools.el.setEnabled($("stream-h264-gop-slider"), enabled);
	};

	var __setInfo = function(is_active, online, text) {
		$("stream-box").classList.toggle("stream-box-offline", !online);
		let el_grab = document.querySelector("#stream-window-header .window-grab");
		let el_info = $("stream-info");
		let title = `${__streamer.getName()} - `;
		if (is_active) {
			if (!online) {
				title += "No video from host / ";
			}
			title += `${__res.width}x${__res.height}`;
			if (text.length > 0) {
				title += " / " + text;
			}
		} else {
			if (text.length > 0) {
				title += text;
			} else {
				title += "No stream from PiKVM";
			}
		}
		el_grab.innerText = title;
		// Заменяем только первое вхождение, всё ок
		el_info.innerText = title.replace(" - ", "\n");
	};

	var __resetStream = function(mode=null) {
		if (mode === null) {
			mode = __streamer.getMode();
		}
		__streamer.stopStream();
		let cls = MjpegStreamer;
		switch (mode) {
			case "janus": cls = JanusStreamer; break;
			case "media": cls = MediaStreamer; break;
		}
		__streamer = new cls(__setActive, __setInactive, __setInfo, __watchHook, __organizeHook);
		if (__isStreamRequired()) {
			__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
			__applyAudioVolume();
			__applyMicEnabled();
			__applyCameraEnabled();
		}
	};

	var __clickModeRadio = function() {
		let mode = tools.radio.getValue("stream-mode-radio");
		tools.storage.set("stream.mode", mode);
		if (mode !== __streamer.getMode()) {
			tools.hidden.setVisible($("stream-canvas"), (mode === "media"));
			tools.hidden.setVisible($("stream-image"), (mode === "mjpeg"));
			tools.hidden.setVisible($("stream-video"), (mode === "janus"));
			__resetStream(mode);
		}
	};

	var __clickScreenshotButton = function() {
		tools.windowOpen("api/streamer/snapshot");
	};

	var __clickResetButton = function() {
		wm.confirm("Are you sure you want to reset the stream?").then(function(ok) {
			if (ok) {
				__resetStream();
				tools.httpPost("api/streamer/reset", null, function(http) {
					if (http.status !== 200) {
						wm.error("Can't reset stream", http.responseText);
					}
				});
			}
		});
	};

	var __sendParam = function(name, value) {
		tools.httpPost("api/streamer/set_params", {[name]: value}, function(http) {
			if (http.status !== 200) {
				wm.error("Can't configure stream", http.responseText);
			}
		});
	};

	__init__();
}
