/* Copyright (C) 2019-2023 m-privacy GmbH. All Rights Reserved.
 *
 * 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.
 */

#ifndef WIN32

#include <atomic>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <string>
#include <sys/wait.h>

#include <tgpro_environment.h>

#include <rfb/LogWriter.h>
#include <rfb/SWebcamHandler.h>

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

threadMap_t SWebcamHandler::ffmpegThreadId;

THREAD_ID SWebcamHandler::fifoReaderThreadId;
int SWebcamHandler::fifoReaderPid;
THREAD_ID fallbackStillFrameLoopThreadId;
bool SWebcamHandler::oldPacketType;

THREAD_ID SWebcamHandler::fallbackStillFrameLoopThreadId = 0;

std::atomic<bool> stopFallbackStillFrameLoopThread { false };

THREAD_FUNC SWebcamHandler::fifoReaderThread(void* _webcamHandler) {
	SWebcamHandler* webcamHandler = (SWebcamHandler*) _webcamHandler;

	vlog.debug("%s: starting fifo reader thread to use ffmpeg to read from FIFO and write to ~/.video-dev", __func__);

	webcamHandler->fifoReaderPid = fork();
	if (!webcamHandler->fifoReaderPid) {
		const char* home = getenv("HOME");
		if (!home) {
			vlog.error("%s: $HOME is not set", __func__);
			THREAD_EXIT(THREAD_NULL);
		}

		char fifoPath[4096], videoDev[4096];
		snprintf(fifoPath, sizeof(fifoPath), "%s/.video-fifo", home);
		snprintf(videoDev, sizeof(videoDev), "%s/.video-dev", home);

		// Run ffmpeg command (/usr/bin/ffmpeg -nostdin -re -f rawvideo -pix_fmt yuv420p -s 1920x1080 -r 30 -i ~/.video-fifo -vf "format=yuv420p" -f v4l2 ~/.video-dev)
		execl(
			   "/usr/bin/ffmpeg", "/usr/bin/ffmpeg",
			   "-nostdin",
			   "-re",
			   "-f", "rawvideo",
			   "-pix_fmt", "yuv420p",
			   "-s", "1280x720",
			   "-r", "30",
			   "-i", fifoPath,
			   "-vf", "format=yuv420p",
			   "-f", "v4l2",
			   videoDev,
			   (char*) NULL
		);

		vlog.error("%s: execlp(ffmpeg) failed", __func__);
		THREAD_EXIT(THREAD_NULL);
	}

	if (webcamHandler->fifoReaderPid < 0) {
		vlog.error("%s: failed to fork ffmpeg reader, error %s", __func__, strerror(errno));
		THREAD_EXIT(THREAD_NULL);
	}

	vlog.debug("%s: ffmpeg reader started with PID %d", __func__, webcamHandler->fifoReaderPid);
	THREAD_EXIT(THREAD_NULL);
}

void sleepWithInterruptCheck(int totalSeconds, const std::atomic<bool>& stopFlag) {
	const unsigned int pollIntervalInMs = 100;

	int iterations = totalSeconds * 1000 / pollIntervalInMs;
	for (int i = 0; i < iterations && !stopFlag.load(); ++i) {
		usleep(pollIntervalInMs * 1000);
	}
}

THREAD_FUNC SWebcamHandler::fallbackStillFrameLoopThread(void* _webcamHandler) {
	SWebcamHandler* webcamHandler = (SWebcamHandler*) _webcamHandler;

	vlog.debug("%s: starting fallback raw frame sender", __func__);

	// Create still frame using ffmpeg so that we can 'cat' it into .video-fifo directly (and practically use no CPU)
	// ffmpeg -i you-did-not-say-the-magic-word.jpg -pix_fmt yuv420p -s 1280x720 -f rawvideo still_frame.yuv
	const char* framePath = "/usr/share/tightgatevncserver/webcam_dummy.yuv";
	const char* home = getenv("HOME");
	if (!home) {
		vlog.error("%s: $HOME is not set", __func__);
		THREAD_EXIT(THREAD_NULL);
	}

	char fifoPath[4096];
	snprintf(fifoPath, sizeof(fifoPath), "%s/.video-fifo", home);

	vlog.debug("%s: creating FIFO to feed ~/.video-dev with it", __func__);
	if (access(fifoPath, F_OK) == -1) {
		if (mkfifo(fifoPath, 0666) == -1) {
			vlog.error("%s: mkfifo in '%s' failed", __func__, fifoPath);
			THREAD_EXIT(THREAD_NULL);
		}
	}

	// Load raw frame once into memory
	FILE* frameFile = fopen(framePath, "rb");
	if (!frameFile) {
		vlog.error("%s: Failed to open frame file %s: %s", __func__, framePath, strerror(errno));
		THREAD_EXIT(THREAD_NULL);
	}

	fseek(frameFile, 0, SEEK_END);

	long fsize = ftell(frameFile);
	if (fsize <= 0) {
		vlog.error("%s: Invalid frame size reported: %ld", __func__, fsize);
		fclose(frameFile);
		THREAD_EXIT(THREAD_NULL);
	}
	size_t frameSize = (size_t) fsize;

	rewind(frameFile);

	uint8_t* frameData = (uint8_t*) malloc(frameSize);
	if (!frameData) {
		fclose(frameFile);
		vlog.error("%s: Memory allocation failed", __func__);
		THREAD_EXIT(THREAD_NULL);
	}

	if (fread(frameData, 1, frameSize, frameFile) != frameSize) {
		fclose(frameFile);
		free(frameData);
		vlog.error("%s: Failed to read entire frame file", __func__);
		THREAD_EXIT(THREAD_NULL);
	}
	fclose(frameFile);

	vlog.debug("%s: Loaded raw frame from %s (%ld bytes)", __func__, framePath, frameSize);

	// Open FIFO for writing (blocking call if no reader yet)
	int fifoFd = open(fifoPath, O_WRONLY);
	if (fifoFd < 0) {
		vlog.error("%s: Failed to open FIFO %s: %s", __func__, fifoPath, strerror(errno));
		free(frameData);
		THREAD_EXIT(THREAD_NULL);
	}

	while (!stopFallbackStillFrameLoopThread) {
		if (webcamHandler->udpPort != 0) {
			vlog.verbose("%s: ffmpeg thread detected, not sending fallback webcam dummy frame", __func__);
			sleepWithInterruptCheck(5, stopFallbackStillFrameLoopThread); // Increase sleep period to 5 seconds if ffmpeg thread detected
			continue;
		}

		ssize_t written = write(fifoFd, frameData, frameSize);
		if (written < 0) {
			vlog.error("%s: Write to FIFO failed: %s", __func__, strerror(errno));
		} else if ((size_t) written != frameSize) {
			vlog.error("%s: Incomplete frame write: %zd of %zu bytes", __func__, written, frameSize);
		}
		sleepWithInterruptCheck(1, stopFallbackStillFrameLoopThread); // "Framerate": 1 second
	}

	close(fifoFd);
	free(frameData);

	vlog.debug("%s: Interrupted. Closed FIFO", __func__);
	THREAD_EXIT(THREAD_NULL);
}

SWebcamHandler::SWebcamHandler(rfb::SSecurityMulti* multi)
	: minPort(50000), portRange(10000)
{
	vlog.debug("%s: Creating SWebcamHandler", __func__);

	userVideoDev = getUserVideoDev();
	oldPacketType = false;
	udpPort = 0;

	rdr::U8 webcamEnabled;
	if (!userVideoDev || !symlink_exists(userVideoDev)) {
		vlog.debug("%s: /home/user/$USER/.video-dev (%s) does not exist, i.e. webcam is *not* enabled.", __func__, userVideoDev);
		webcamEnabled = 0;
	} else {
		vlog.debug("%s: /home/user/$USER/.video-dev exists, i.e. webcam is enabled (and the user has an assigned virtual video device).", __func__);
		webcamEnabled = 1;
		vlog.debug("%s: starting thread that constantly reads from fifo and writes to video dev using ffmpeg", __func__);
		THREAD_CREATE(fifoReaderThread, fifoReaderThreadId, (void *) this);
		vlog.debug("%s: Starting fallback still frame loop (with the press-f8 picture)", __func__);
		THREAD_CREATE(fallbackStillFrameLoopThread, fallbackStillFrameLoopThreadId, (void *) this);
	}

	vlog.debug("%s: Informing the user with WEBCAM_ENABLED_SIGNAL_ID (webcamEnabled=%i)", __func__, webcamEnabled);
	multi->sendSignal(WEBCAM_ENABLED_SIGNAL_ID, &webcamEnabled, 1); // Tell the client that it has a video device assigned
}

SWebcamHandler::~SWebcamHandler()
{
	vlog.debug("%s: destroying SWebcamHandler object", __func__);
	if (userVideoDev) {
		free(userVideoDev);
	}

	vlog.debug("%s: telling fallback still frame thread to stop and waiting for it", __func__);
	stopFallbackStillFrameLoopThread = true;
	THREAD_JOIN(fallbackStillFrameLoopThreadId);

	/* wait for ffmpeg threads */
	udpPort = 0;
	for (auto iter = ffmpegThreadId.begin(); iter != ffmpegThreadId.end(); ++iter) {
		vlog.debug("%s: waiting for ffmpeg thread %lu for port %u", __func__, iter->second, iter->first);
		THREAD_JOIN(iter->second);
	}
	if (fifoReaderPid > 0) {
		if (kill(fifoReaderPid, SIGTERM) < 0)
			vlog.debug("%s: killing FIFO reader thread with pid %u", __func__, fifoReaderPid);
		sleep(0.2);
		if(!kill(fifoReaderPid, 0)) {
			vlog.error("%s: FIFO reader thread with pid %u still alive after 0.2s, killing hard now", __func__, fifoReaderPid);
			if (kill(fifoReaderPid, SIGKILL) < 0)
				vlog.error("%s: killing FIFO reader thread with pid %u failed again with error %s", __func__, fifoReaderPid, strerror(errno));
		}
		fifoReaderPid = -1;
	}
	/* remove fifo file  */
	char fifoPath[4096];
	snprintf(fifoPath, 4095, "%s/.video-fifo", getenv("HOME"));
	if (unlink(fifoPath) == -1) {
		if (errno != ENOENT) {
			vlog.error("%s: Deleting fifo '%s' error: %s", __func__, fifoPath, strerror(errno));
		}
	} else {
		vlog.error("%s: Deleted fifo '%s'", __func__, fifoPath);
	}
}

char* SWebcamHandler::getUserVideoDev() {
	const char* username = getenv("USER");

	if (!username) {
		vlog.error("%s: Couldn't read username from enviroment $USER", __func__);
		return NULL;
	}

	const int userVideoDevFilenameSize = strlen(username) + strlen("/home/user//.video-dev") + 1;
	char* userVideoDev = (char*) malloc(userVideoDevFilenameSize);
	if (!userVideoDev) {
		vlog.error("%s: Couldn't allocate memory for userVideoDev name", __func__);
		return NULL;
	}

	snprintf(userVideoDev, userVideoDevFilenameSize, "/home/user/%s/.video-dev", username);
	vlog.debug("%s: userVideoDev for user: %s", __func__, userVideoDev);
	return userVideoDev;
}

#define MAXUDPCHUNK 64000

bool SWebcamHandler::commonHandlePacket(const rdr::U8* buf, int bufLen) {
	if (udpPort == 0) {
		vlog.debug("%s: Trying to find a random UDP port to forward webcam stream", __func__);
		udpPort = findUdpPort();
	}
	if (!ffmpegThreadId[udpPort]) {
		char name[16];

		vlog.debug("%s: no ffmpeg thread running for port %u, starting one", __func__, udpPort);
		THREAD_CREATE(ffmpegThread, ffmpegThreadId[udpPort], (void *) this);
		snprintf(name, 15, "tg-ffmpeg-%u", udpPort);
		name[15] = 0;
		THREAD_SET_NAME(ffmpegThreadId[udpPort], name);
	}

	int sockfd;
	struct sockaddr_in servaddr;

	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		vlog.error("%s: UDP socket creation failed", __func__);
		return false;
	}

	lastTime[udpPort] = time(NULL);

	memset(&servaddr, 0, sizeof(servaddr));

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(udpPort);
	servaddr.sin_addr.s_addr = INADDR_ANY;

	int sent = 0;
	int chunksize;

	while (sent < bufLen) {
		chunksize = bufLen - sent;
		if (chunksize > MAXUDPCHUNK) {
			vlog.debug("%s: need to split huge buffer of size %u", __func__, bufLen);
			chunksize = MAXUDPCHUNK;
		}
		if (sendto(sockfd, buf + sent, chunksize, MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr)) == -1) {
			vlog.error("%s: error sending packet chunk of size %u to UDP socket on port %i: %s", __func__, chunksize, udpPort, strerror(errno));
			close(sockfd);
			return true;
		}
		sent += chunksize;
	}

	close(sockfd);

	return true;
}

bool SWebcamHandler::handlePacket(const rdr::U8* buf, int bufLen)
{
	oldPacketType = false;
	return commonHandlePacket(buf, bufLen);
}

bool SWebcamHandler::handleOldPacket(const rdr::U8* buf, int bufLen)
{
	oldPacketType = true;
	return commonHandlePacket(buf, bufLen);
}

THREAD_FUNC SWebcamHandler::ffmpegThread(void* _webcamHandler) {
	SWebcamHandler* webcamHandler = (SWebcamHandler*) _webcamHandler;
	int ffmpegPid;
	time_t currentTime;
	int port = webcamHandler->udpPort;

	vlog.debug("%s: starting ffmpeg process on port %u", __func__, port);

	ffmpegPid = fork();
	if (!ffmpegPid) {
		char videodev[4096], videoFifo[4096];

		fclose(stdin);
		fclose(stdout);
		fclose(stderr);
		snprintf(videodev, 4095, "%s/.video-dev", getenv("HOME"));
		snprintf(videoFifo, 4095, "%s/.video-fifo", getenv("HOME"));

		if (oldPacketType) {
			char rtp[64];

			snprintf(rtp, 63, "rtp://127.0.0.1:%i", port);
			execl("/usr/bin/ffmpeg", "/usr/bin/ffmpeg", "-nostdin", "-i", rtp, "-vsync",  "2", "-pix_fmt", "yuv420p", "-f", "v4l2", videodev, NULL);
		} else {
			char udp[64];
			snprintf(udp, 63, "udp://127.0.0.1:%i", port);

			const char* ffmpegPath = "/usr/bin/ffmpeg";

			const char* const ffmpegArgs[] = {
				ffmpegPath,
				"-y",
				"-nostdin",
				"-fflags", "+nobuffer+genpts",
				"-use_wallclock_as_timestamps", "1",
				"-f", "mpegts",
				"-i", udp,
				"-pix_fmt", "yuv420p",
				"-s", "1280x720",
				"-f", "rawvideo",
				videoFifo,
				NULL
			};

			std::string cmdStr;
			for (int i = 0; ffmpegArgs[i]; ++i) {
				cmdStr += "'";
				cmdStr += ffmpegArgs[i];
				cmdStr += "' ";
			}

			vlog.debug("%s: trying to run ffmpeg command: %s", __func__, cmdStr.c_str());

			execv(ffmpegPath, (char* const*) ffmpegArgs);
		}
		/* we should never reach this point */
		vlog.error("%s: failed to execute ffmpeg command, error %s", __func__, strerror(errno));
		exit(0);
	}
	if (ffmpegPid < 0) {
		vlog.error("%s: failed to fork ffmpeg command, error %s", __func__, strerror(errno));
		THREAD_EXIT(THREAD_NULL);
	}

	vlog.debug("%s: ffmpeg command started with pid %u for port %u, watching now", __func__, ffmpegPid, port);

	while (true) {
		currentTime = time(NULL);
		if(currentTime > webcamHandler->lastTime[port] + WEBCAM_WAIT_TIMEOUT_IN_SEC) {
			vlog.debug("%s: ffmpeg with pid %u on port %u inactive, killing now", __func__, ffmpegPid, port);
			break;
		}
		if(port != webcamHandler->udpPort) {
			vlog.debug("%s: ffmpeg with pid %u on port %u, but port changed to %u, killing now", __func__, ffmpegPid, port, webcamHandler->udpPort);
			break;
		}
//		vlog.debug("%s: ffmpeg with pid %u on port %u: currentTime %lu, lastTime[%u] %lu", __func__, ffmpegPid, port, currentTime, port, webcamHandler->lastTime[port]);
		sleep(2);
	}
	if (kill(ffmpegPid, SIGTERM) < 0)
		vlog.debug("%s: killing ffmpeg with pid %u on port %u failed with error %s", __func__, ffmpegPid, port, strerror(errno));
	sleep(2);
	if(!kill(ffmpegPid, 0)) {
		vlog.error("%s: ffmpeg with pid %u on port %u still alive after 2s, killing hard now", __func__, ffmpegPid, port);
		if (kill(ffmpegPid, SIGKILL) < 0)
			vlog.error("%s: killing ffmpeg with pid %u on port %u failed again with error %s", __func__, ffmpegPid, port, strerror(errno));
	}
	webcamHandler->lastTime.erase(port);
	webcamHandler->ffmpegThreadId.erase(port);
	waitpid(ffmpegPid, NULL, 0);
	THREAD_EXIT(THREAD_NULL);
}

int SWebcamHandler::findUdpPort()
{
	int sockfd;
	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		vlog.error("%s: failed to create a socket", __func__);
		return 0;
	}

	struct sockaddr_in addr;
	srand((unsigned) time(NULL));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = INADDR_ANY;
	unsigned int maxTries = 50;
	unsigned int i;
	int bound_sock = -1;
	for (i = 0; i < maxTries && bound_sock < 0; i++) {
		double randomScaler = (double) rand() / ((double) RAND_MAX + 1.0);
		addr.sin_port = (double) minPort + randomScaler * (double) portRange; // TODO take a look at this random generation
		vlog.debug("%s: Socket run try no %i: Port %i", __func__, i + 1, addr.sin_port);
		bound_sock = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
	}
	int freePort = addr.sin_port;
	close(sockfd);

	if (i == maxTries) {
		vlog.error("%s: failed to find a free UDP port (tried %i times)", __func__, maxTries);
		return 0;
	}

	return freePort;
}

void SWebcamHandler::setClientSupportsWebcam(bool value)
{
	vlog.debug("%s: client supports webcam: %s", __func__, value ? "yes" : "no");
	clientSupportsWebcam = value;

	if (!clientSupportsWebcam) {
		// The user just switched the webcam off and notified us (ffmpeg would stop anyway, as we will very soon stop receiving webcam packets. Like this, we stop the process as ASAP)
		// The case were the user enables the webcam is uninteresting, as the logic to start the threads is in the handlePacket function (maybe we should change this)
		udpPort = 0;
	}
}

#endif /* #ifndef WIN32 */
