/*
 * Copyright (C) 2019-2021 m-privacy GmbH
 *
 * This 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 2 of the License, or
 * (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#include <rfb/CWebcamHandler.h>

#include <rfb/LogWriter.h>
#include <rfb/Configuration.h>
#ifdef WIN32
#include <rfb/WinErrors.h>
#endif

#include <vncviewer/parameters.h>

#include <tgpro_environment.h>

#ifdef WIN32
#include <tlhelp32.h>
#else
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#endif

static rfb::LogWriter vlog("CWebcamHandler");

THREAD_ID CWebcamHandler::webcamSocketThreadId = (THREAD_ID) NULL;
THREAD_ID CWebcamHandler::webcamEnablerThreadId = (THREAD_ID) NULL;

TGVNC_CONDITION_TYPE CWebcamHandler::webcamEnabledCondition;
MUTEX_TYPE CWebcamHandler::webcamEnabledConditionLock;

TGVNC_CONDITION_TYPE CWebcamHandler::udpPortCondition;
MUTEX_TYPE CWebcamHandler::udpPortConditionLock;

rfb::StringParameter CWebcamHandler::webcamExtraParam("WebcamExtraParam", "extra parameters to ffmpeg", "", rfb::ConfViewer);

int CWebcamHandler::ffmpegPid;
static int webcamUdpPortInt;

CWebcamHandler::CWebcamHandler(rdr::MultiOutStream* multios)
	: multios(multios)
#if !defined(__APPLE__)
	, ffmpegExe(NULL)
#endif
{
	vlog.debug("%s: Creating CWebcamHandler", __func__);

	MUTEX_INIT(&udpPortConditionLock);
	TGVNC_CONDITION_INIT(&udpPortCondition);
	MUTEX_INIT(&webcamEnabledConditionLock);
	TGVNC_CONDITION_INIT(&webcamEnabledCondition);

	lastWebcamEnabledValue = webcamEnabled;
	ffmpegPid = 0;
#if 0
	lastMicEnabledValue = micEnabled;
#endif

#ifdef WIN32
	ffmpegExe = file_exists_in_path(get_start_path(), "ffmpeg.exe");
	if (!ffmpegExe) {
		ffmpegExe = file_exists_in_path(get_tgpro_path(), "ffmpeg.exe");
	}
#endif

	THREAD_CREATE(webcamEnablerThread, webcamEnablerThreadId, (void*) this);
	THREAD_SET_NAME(webcamEnablerThreadId, "tg-webcamEnabler");
}

CWebcamHandler::~CWebcamHandler() {
	vlog.debug("%s: destroying CWebcamHandler object", __func__);
	killAllFfmpegProcesses();
	TGVNC_CONDITION_DESTROY(&webcamEnabledCondition);
	MUTEX_DESTROY(&webcamEnabledConditionLock);
	TGVNC_CONDITION_DESTROY(&udpPortCondition);
	MUTEX_DESTROY(&udpPortConditionLock);
	if (webcamEnablerThreadId) {
		THREAD_CANCEL(webcamEnablerThreadId);
	}
	if (webcamSocketThreadId) {
		THREAD_CANCEL(webcamSocketThreadId);
	}
}

THREAD_FUNC CWebcamHandler::webcamEnablerThread(void* _webcamHandler)
{
	CWebcamHandler* webcamHandler = (CWebcamHandler*) _webcamHandler;
	if (!webcamHandler) {
		vlog.error("%s: CWebcamHandler Object from parent thread can't be NULL", __func__);
		return (THREAD_FUNC) NULL;
	}

	while (42 == 42) {
		MUTEX_LOCK(&webcamEnabledConditionLock);
		vlog.debug("%s: waiting for user to enable or disable the webcam (a 'webcamEnabled' change)...", __func__);
		TGVNC_CONDITION_WAIT(&webcamEnabledCondition, &webcamEnabledConditionLock);
		MUTEX_UNLOCK(&webcamEnabledConditionLock);

		if (   (webcamEnabled != webcamHandler->lastWebcamEnabledValue)
#if 0
		    || (micEnabled != webcamHandler->lastMicEnabledValue)
#endif
		   ) {
			webcamHandler->lastWebcamEnabledValue = webcamEnabled;
#if 0
			webcamHandler->lastMicEnabledValue = micEnabled;
#endif
			rdr::U8 webcamEnabledSignal = (rdr::U8) webcamEnabled ? 1 : 0;
			vlog.debug("%s: Informing the server with WEBCAM_ENABLED_SIGNAL_ID (webcamEnabled=%i)", __func__, webcamEnabledSignal);
			webcamHandler->multios->sendSignal(WEBCAM_ENABLED_SIGNAL_ID, &webcamEnabledSignal, 1);

			if (webcamEnabled) {
				vlog.debug("%s: Enabled webcam", __func__);
				if (!webcamSocketThreadId) {
					vlog.debug("%s: Webcam is enabled and there isn't a thread to open a udp socket. Starting one...", __func__);
					THREAD_CREATE(webcamSocketThread, webcamSocketThreadId, (void*) webcamHandler);
					THREAD_SET_NAME(webcamSocketThreadId, "tg-webcamSocket");
				}
				else {
					vlog.debug("%s: UDP socket thread already running. Not starting a new thread", __func__);
				}

#if 0
				if (micEnabled != webcamHandler->lastMicEnabledValue && webcamHandler->ffmpegPid) {
					vlog.debug("%s: mic setting changed, kill old ffmpeg to force restart", __func__);
					webcamHandler->stopFfmpeg();
					webcamHandler->killAllFfmpegProcesses();
				}
#endif
				if (!webcamHandler->ffmpegPid) { // We don't need a thread for this. 'launch_program' starts an external program as its own process
					vlog.debug("%s: Webcam is enabled and there isn't a ffmpeg process. Starting one...", __func__);
					MUTEX_LOCK(&udpPortConditionLock);
					vlog.debug("%s: Waiting for available random local UDP port before starting ffmpeg", __func__);
					TGVNC_CONDITION_WAIT(&udpPortCondition, &udpPortConditionLock);
					MUTEX_UNLOCK(&udpPortConditionLock);
					webcamHandler->startFfmpeg(); // This sets ffmpeg pid.
				}
			} else {
				vlog.debug("%s: Disabled webcam", __func__);
				vlog.debug("%s: Stopping ffmpeg (video capture)", __func__);
				webcamHandler->stopFfmpeg();
				webcamHandler->killAllFfmpegProcesses();
				if (webcamSocketThreadId) {
					vlog.debug("%s: Stopping listening on UDP port (canceling webcamSocketThread)", __func__);
					THREAD_CANCEL(webcamSocketThreadId);
					webcamSocketThreadId = (THREAD_ID) NULL;
				}

			}
		}
	}
	return (THREAD_FUNC) NULL;
}

THREAD_FUNC CWebcamHandler::webcamSocketThread(void* _webcamHandler)
{
	char * buf;

	CWebcamHandler* webcamHandler = (CWebcamHandler*) _webcamHandler;
	if (!webcamHandler) {
		vlog.error("%s: CWebcamHandler object from parent thread can't be NULL", __func__);
		return (THREAD_FUNC) NULL;
	}

#ifdef WIN32
	int slen;
	SOCKET webcamSocket;
	struct sockaddr_in si_server, si_client;
	WSADATA wsa;

	vlog.debug("%s: Initialising winsock...", __func__);
	if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
		vlog.error("%s: WSAStartup() failed with error Code: %d. Forcing disabling webcam.", __func__, WSAGetLastError());
		webcamEnabled.setParam(false);
		return (THREAD_FUNC) NULL;
	}
	slen = sizeof(si_client);
	webcamSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (webcamSocket == INVALID_SOCKET)
	{
		vlog.error("%s: socket() failed with error code: %d. Forcing disabling webcam.", __func__, WSAGetLastError());
		WSACleanup();
		webcamEnabled.setParam(false);
		return (THREAD_FUNC) NULL;
	}
	si_server.sin_family = AF_INET;
	si_server.sin_addr.s_addr = inet_addr("127.0.0.1");

	srand((unsigned) time(NULL));
	const unsigned int maxTries = 50;
	const unsigned int minPort = 50000;
	const unsigned int portRange = 10000;
	unsigned int i;
	int boundSock = SOCKET_ERROR;
	for (i = 0; i < maxTries && boundSock == SOCKET_ERROR; i++) {
		double randomScaler = (double) rand() / ((double) RAND_MAX + 1.0);
		webcamUdpPortInt = (double) minPort + randomScaler * (double) portRange;
		si_server.sin_port = htons(webcamUdpPortInt);
		vlog.debug("%s: trying to bind to UDP socket using port %i (try no %i out of %i)", __func__, webcamUdpPortInt, i + 1, maxTries);
		boundSock = bind(webcamSocket, (struct sockaddr *)&si_server, sizeof(si_server));
		if (boundSock == SOCKET_ERROR) {
			vlog.error("%s: bind failed with error code: %d.", __func__, webcamUdpPortInt);
		}
	}

	if (boundSock == SOCKET_ERROR) {
		vlog.error("Bind UDP socket for webcam failed %i times. Giving up and disabling webcam.", maxTries);
		WSACleanup();
		webcamEnabled.setParam(false);
		return (THREAD_FUNC) NULL;
	} else {
		vlog.debug("%s: successfully bound to port %i", __func__, webcamUdpPortInt);
		TGVNC_CONDITION_SEND_SIG(&udpPortCondition);
	}
#else
	int webcamSocket;
	struct sockaddr_in si_server;
	webcamSocket = socket(AF_INET, SOCK_DGRAM, 0);
	if (webcamSocket  < 0)
	{
		vlog.error("%s: socket() failed with error: %s. Forcing disabling webcam.", __func__, strerror(errno));
		webcamEnabled.setParam(false);
		THREAD_EXIT(THREAD_NULL);
	}
	si_server.sin_family = AF_INET;
	si_server.sin_addr.s_addr = inet_addr("127.0.0.1");

	srand((unsigned) time(NULL));
	const unsigned int minPort = 50000;
	const unsigned int portRange = 10000;
	const unsigned int maxTries = 50;
	unsigned int i;
	int boundSock = -1;
	for (i = 0; i < maxTries && boundSock < 0; i++) {
		double randomScaler = (double) rand() / ((double) RAND_MAX + 1.0);
		webcamUdpPortInt = (double) minPort + randomScaler * (double) portRange;
		si_server.sin_port = htons(webcamUdpPortInt);
		vlog.debug("%s: trying to bind to UDP socket using port %i (try no %i out of %i)", __func__, si_server.sin_port, i + 1, maxTries);
		boundSock = bind(webcamSocket, (struct sockaddr *)&si_server, sizeof(si_server));
		if (boundSock < 0) {
			vlog.error("Bind UDP socket to port %u failed: %s.", webcamUdpPortInt, strerror(errno));
		}
	}

	if (boundSock < 0) {
		vlog.error("Bind UDP socket for webcam failed %i times. Giving up and disabling webcam", maxTries);
		close(webcamSocket);
		webcamSocket = -1;
		THREAD_EXIT(THREAD_NULL);
	} else {
		vlog.debug("%s: successfully bound to port %i", __func__, webcamUdpPortInt);
		TGVNC_CONDITION_SEND_SIG(&udpPortCondition);
	}

#endif

	buf = (char *) malloc(MAX_DATAGRAM_LEN);
	if (!buf) {
		vlog.error("could not allocate buffer");
#ifdef WIN32
		WSACleanup();
#endif
		webcamEnabled.setParam(false);
		THREAD_EXIT(THREAD_NULL);
	}

	// closesocket() always fails with error "0", which doesn't make any sense. That's why we're leaving
	// it open and using an infinite loop and just ignoring packets, should they still arrive.
	vlog.debug("%s: Listening for data...", __func__);
	while (webcamEnabled) {
//		memset(buf, 0, MAX_DATAGRAM_LEN); // clear buffer just in case it still has data

#ifdef WIN32
		fd_set fds;
		FD_ZERO(&fds);
		FD_SET(webcamSocket, &fds);

		struct timeval timeout;
		timeout.tv_sec = 10;
		timeout.tv_usec = 0;

		// Wait until timeout or data received. This allows to only go into the blocking recvfrom only if there is data to receive.
		const int n = select(webcamSocket + 1, &fds, NULL, NULL, &timeout);
		if (n == 0) {
			// select() timed out. Not running recvfrom(). Continuing with while loop (if webcamEnabled is still true)
			continue;
		} else if (n == -1 ) {
			vlog.error("%s: select() failed with error: %s, abort receiving in UDP socket.", __func__, strerror(errno));
			break;
		}
#endif

		// select() succeeded. There is data waiting in the UDP socket

#ifdef WIN32
		int bytesReceived = recvfrom(webcamSocket, buf, MAX_DATAGRAM_LEN, 0, (struct sockaddr*) &si_client, &slen);
		if (bytesReceived == SOCKET_ERROR) {
			bytesReceived = WSAGetLastError();
			vlog.error("%s: recvfrom() failed with error code: %d (%s). Abort receiving in UDP socket", __func__, bytesReceived, WinErrorName(bytesReceived));
			break;
		}
		if (bytesReceived == 0) {
			vlog.debug("%s: recvfrom() received 0 bytes, sleep 1s and continue", __func__);
			sleep(1);
			continue;
		}
		if (webcamEnabled) {
			webcamHandler->multios->writeBuffer((rdr::U8*) buf, bytesReceived, WEBCAM_VIDEO_STREAM_ID, true);
		} else {
			vlog.debug("%s: for some reason, we are receiving packets in the UDP socket although the webcam is disabled. Do not worry, they are not being sent to your TG-Pro instance. Perhaps a ffmpeg instance is still running?", __func__);
		}
#else
		int bytesReceived = recvfrom(webcamSocket, buf, MAX_DATAGRAM_LEN, 0, NULL, NULL);
		if (bytesReceived < 0) {
			vlog.error("%s: recvfrom() failed with error: %s. Abort receiving in UNIX socket", __func__, strerror(errno));
			break;
		}
		if (bytesReceived == 0) {
			vlog.debug("%s: recvfrom() received 0 bytes, sleep 1s and continue", __func__);
			sleep(1);
			continue;
		}
//		vlog.debug("%s: recvfrom() got %u bytes", __func__, bytesReceived);
		if (webcamEnabled) {
//			vlog.debug("%s: sending %u bytes", __func__, bytesReceived);
			webcamHandler->multios->writeBuffer((rdr::U8*) buf, bytesReceived, WEBCAM_VIDEO_STREAM_ID, true);
		} else {
			vlog.debug("%s: for some reason, we are receiving packets in the UDP socket although the webcam is disabled. Do not worry, they are not being sent to your TG-Pro instance. Perhaps a ffmpeg instance is still running?", __func__);
		}
#endif
	}

	free(buf);
	vlog.debug("%s: Webcam disabled (or while loop broken due to error). Closing socket", __func__);
	webcamEnabled.setParam(false);
#ifdef WIN32
	if (closesocket(webcamSocket) == SOCKET_ERROR) {
		vlog.error("%s: closesocket() failed with error code: %d", __func__, WSAGetLastError());
	}
	WSACleanup();
#else
	if (close(webcamSocket) < 0) {
		vlog.error("%s: close(webcamSocket) failed with error: %s", __func__, strerror(errno));
	}
#endif
	THREAD_EXIT(THREAD_NULL);
}

void CWebcamHandler::startFfmpeg()
{
	char command[4096];

#ifdef WIN32
	if (!file_exists(ffmpegExe)) {
		vlog.debug("%s: ffmpeg executable doesn't exist: %s", __func__, ffmpegExe);
		return;
	}
	if (!strcmp(webcamSize.getValueStr(), "MAX"))
		snprintf(command, 4095, "\"%s\" -fflags nobuffer -f dshow -i video=\"%s\" -thread_type slice -slices 1 -r 30 -g 60 -acodec aac -ar 44100 -b:v 2.5M -minrate:v 900k -maxrate:v 5M -bufsize:v 5M -b:a 128K -pix_fmt yuv420p -f mpegts %s udp://127.0.0.1:%u?pkt_size=1316", ffmpegExe, webcamName.getValueStr(), webcamExtraParam.getValueStr(), webcamUdpPortInt);
	else
		snprintf(command, 4095, "\"%s\" -fflags nobuffer -f dshow -i video=\"%s\" -thread_type slice -slices 1 -r 30 -g 60 -s %s -acodec aac -ar 44100 -b:v 2.5M -minrate:v 900k -maxrate:v 2.5M -bufsize:v 5M -b:a 128K -pix_fmt yuv420p -f mpegts %s udp://127.0.0.1:%u?pkt_size=1316", ffmpegExe, webcamName.getValueStr(), webcamSize.getValueStr(), webcamExtraParam.getValueStr(), webcamUdpPortInt);
#else /* Code for unixoid OSs: */
	char redirecterr[50];

	if(vlog.getLevel() >= vlog.LEVEL_DEBUG) {
		sprintf(redirecterr, "/tmp/ffmpeg-%u.log", getpid());
		unlink(redirecterr);
	} else {
		sprintf(redirecterr, "/dev/null");
	}
	if (!strcmp(webcamSize.getValueStr(), "MAX"))
		snprintf(command, 4095, "ffmpeg -nostdin -fflags nobuffer -f v4l2 -i '%s' -thread_type slice -slices 1 -r 30 -g 60 -acodec aac -ar 44100 -b:v 2.5M -minrate:v 900k -maxrate:v 5M -bufsize:v 5M -b:a 128K -pix_fmt yuv420p -f mpegts %s 'udp://127.0.0.1:%u?pkt_size=1316' >/dev/null 2>%s", webcamName.getValueStr(), webcamExtraParam.getValueStr(), webcamUdpPortInt, redirecterr);
	else
		snprintf(command, 4095, "ffmpeg -nostdin -fflags nobuffer -f v4l2 -i '%s' -thread_type slice -slices 1 -r 30 -g 60 -s %s -acodec aac -ar 44100 -b:v 2.5M -minrate:v 900k -maxrate:v 2.5M -bufsize:v 5M -b:a 128K -pix_fmt yuv420p -f mpegts %s 'udp://127.0.0.1:%u?pkt_size=1316' >/dev/null 2>%s", webcamName.getValueStr(), webcamSize.getValueStr(), webcamExtraParam.getValueStr(), webcamUdpPortInt, redirecterr);
#endif

	command[4095] = 0;

	vlog.debug("%s: Starting ffmpeg with: <%s>", __func__, command);
#ifdef WIN32
	ffmpegPid = launch_program(command, 0, 0, 0, NULL);
	if (!ffmpegPid) {
		vlog.error("%s: Failed to start ffmpeg", __func__);
	} else {
		vlog.debug("%s: ffmpeg started with pid %u", __func__, ffmpegPid);
	}
#else /* Code for unixoid OSs: */
	ffmpegPid = fork();
	if (!ffmpegPid) {
//		daemon(0, 0);
		system(command);
		vlog.debug("%s: ffmpeg finished.", __func__);
		exit(0);
	}
	if (ffmpegPid < 0) {
		vlog.error("%s: Failed to fork for ffmpeg", __func__);
	} else {
		vlog.debug("%s: ffmpeg started with pid %u", __func__, ffmpegPid);
	}
#endif
}

void CWebcamHandler::stopFfmpeg() {
	if (!ffmpegPid) {
		vlog.debug("%s: ffmpeg pid is 0. Not killing it.", __func__);
		return;
	}

	vlog.debug("%s: Killing ffmpeg process with pid %u", __func__, ffmpegPid);
	kill_process(ffmpegPid);
	/* kill the hard way, just in case */
#ifdef WIN32
	if (webcamEnabled)
	        system("taskkill /f /IM ffmpeg.exe");
#else
	if (webcamEnabled)
	        system("killall ffmpeg");
#endif
	ffmpegPid = 0;
}

void CWebcamHandler::killAllFfmpegProcesses()
{
	// Sometimes, ffmpeg starts as an independant process (I really don't get why), so we'll just kill all the ffmpeg processes
#ifdef WIN32
	HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);
	PROCESSENTRY32 pEntry;
	pEntry.dwSize = sizeof(pEntry);
	BOOL hRes = Process32First(hSnapShot, &pEntry);
	while (hRes) {
		if (strcmp(pEntry.szExeFile, "ffmpeg.exe") == 0) {
			vlog.debug("%s: Parent process that started the ffmpeg process: %lu", __func__, pEntry.th32ParentProcessID);
			vlog.debug("%s: There is an \"independent\" ffmpeg process (which we probably started anyway) running! Trying to force kill it.", __func__);
			kill_process(pEntry.th32ProcessID);
		}
		hRes = Process32Next(hSnapShot, &pEntry);
	}
	CloseHandle(hSnapShot);
#else
	kill_process(ffmpegPid);
	system("killall ffmpeg");
	ffmpegPid = 0;
#endif /* ifdef WIN32 */
}
