/* Copyright 2017-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.
 */

#ifndef WIN32 /* This is only for the server, so we don't need to build it at all for Windows */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <chrono>
#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <syslog.h>
#include <sys/inotify.h>
#include <sys/wait.h>
#include <tgpro_environment.h>

#include <rfb/LogWriter.h>

#include "SAutotransferDirWatcher.h"

#define INOTIFY_EVENT_SIZE (sizeof (struct inotify_event))
#define INOTIFY_BUFFER_LENGTH (1024 * (INOTIFY_EVENT_SIZE + 16))

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

// We use arrays for these static variables because every implementation should have new locks and condtions for its own queue/s.
MUTEX_TYPE SAutotransferDirWatcher::processingQueueLocks[MAX_THREADS];
TGVNC_CONDITION_TYPE SAutotransferDirWatcher::filesWaitingConditions[MAX_THREADS];
MUTEX_TYPE SAutotransferDirWatcher::filesWaitingConditionLocks[MAX_THREADS];

MUTEX_TYPE SAutotransferDirWatcher::threadCountLock;
unsigned int SAutotransferDirWatcher::threadCount = 0;

bool SAutotransferDirWatcher::clientSupportsAutotransfer = true;

SAutotransferDirWatcher::SAutotransferDirWatcher() {
	MUTEX_INIT(&threadCountLock);
}

SAutotransferDirWatcher::~SAutotransferDirWatcher()
{
	MUTEX_DESTROY(&threadCountLock);
	if (started) {
		TGVNC_CONDITION_DESTROY(&filesWaitingConditions[myThread]);
		MUTEX_DESTROY(&filesWaitingConditionLocks[myThread]);
		MUTEX_DESTROY(&processingQueueLocks[myThread]);
	}
}

void SAutotransferDirWatcher::setLogPathForEvents(std::string path) {
	logPathForEvents = path;
}

void SAutotransferDirWatcher::start()
{
	dirToWatch = getDirToWatch();

	if (!file_exists(dirToWatch.c_str())) {
		vlog.error("%s: Directory '%s' doesn't exist. Can't watch it with inotify!", __func__, dirToWatch.c_str());
		return;
	}

	MUTEX_LOCK(&threadCountLock);
	myThread = threadCount++;
	MUTEX_UNLOCK(&threadCountLock);

	if (myThread >= MAX_THREADS) {
		vlog.error("%s: reached max. number of threads for DirWatchers. Not starting a new one.", __func__);
		return;
	}
	vlog.debug("%s: starting a new SAutotransferDirWatcher thread: %i", __func__, myThread);

	MUTEX_INIT(&filesWaitingConditionLocks[myThread]);
	TGVNC_CONDITION_INIT(&filesWaitingConditions[myThread]);
	MUTEX_INIT(&processingQueueLocks[myThread]);

	// The watches dirToWatch and adds new files to the queue.
	THREAD_ID inotifyWatchDirThreadId;
	THREAD_CREATE(inotifyWatchDirThread, inotifyWatchDirThreadId, (void *) this);
	THREAD_SET_NAME(inotifyWatchDirThreadId, "tg-inotifyWatchDir");

	// This thread watches a std::queue with a lock. The files that inotify finds are added to this queue sequentially. Runs processFile on every file in the queue
	THREAD_ID watchQueueThreadId;
	THREAD_CREATE(watchQueueThread, watchQueueThreadId, (void *) this);
	THREAD_SET_NAME(watchQueueThreadId, "tg-inotifyWatchQueue");

	started = true;
}

THREAD_FUNC SAutotransferDirWatcher::inotifyWatchDirThread(void* _thiss)
{
	SAutotransferDirWatcher* thiss = (SAutotransferDirWatcher*) _thiss;

	vlog.debug("%s: Starting to watch '%s' with inotify...", __func__, thiss->dirToWatch.c_str());
	int fd;
	int wd;

	fd = inotify_init();
	if (fd < 0) {
		vlog.error("inotify_init error %i: %s", errno, strerror(errno));

	}

	vlog.verbose("Adding watch");
	wd = inotify_add_watch(fd, thiss->dirToWatch.c_str(), IN_CLOSE_WRITE | IN_MOVED_TO);
	if (wd < 0) {
		vlog.error("%s: Failed to add watch for %s. Error %i: %s. Stopping autotransfer functionality.", __func__, thiss->dirToWatch.c_str(), errno, strerror(errno));
		THREAD_EXIT(THREAD_NULL);
	}

	char buffer[INOTIFY_BUFFER_LENGTH];
	while(42 == 42) {
		int length, i = 0;

		length = read(fd, buffer, INOTIFY_BUFFER_LENGTH);
		vlog.verbose("%s: Read %i bytes from inotify buffer (max. capacity: %ld)", __func__, length, INOTIFY_BUFFER_LENGTH);

		if (length < 0) {
			vlog.error("%s: read error %i on inotify fd. Stopping autotransfer functionality.", __func__, errno);
			THREAD_EXIT(THREAD_NULL);
		}

		while (i < length) {
			struct inotify_event* event = (struct inotify_event*) &buffer[i];
			if (event->len) {
				if (event->mask & IN_ISDIR) {
					vlog.error("%s: Directories not supported by the autotransfer feature. Ignoring '%s'", __func__, event->name);
				} else {
					if (event->mask & IN_CLOSE_WRITE) {
						vlog.verbose("%s: The file '%s' was 'close_written'.", __func__, event->name);
					} else if (event->mask & IN_MOVED_TO) {
						vlog.verbose("%s: The file '%s' was 'in_moved_to'.", __func__, event->name);
					}
					char* filename = (char*) malloc(4096);
					if (!filename) {
						vlog.error("%s: Failed to allocate memory for filename. Can't add to processing queue", __func__);
					} else {
						snprintf(filename, 4095, "%s/%s", thiss->dirToWatch.c_str(), event->name);
						filename[4095] = '\0';
						if (!clientSupportsAutotransfer) {
							vlog.debug("%s: client AutotransferSupport is set to false (set AutotransferSupport=1 in tgpro.vnc for this to work). Ignoring inotify event of file '%s'", __func__, filename);
						} else {
							thiss->addToProcessQueue(filename);
						}
					}
				}
			} else {
				vlog.error("%s: something strange is happening: the length of the inotify event is 0", __func__);
			}
			i += INOTIFY_EVENT_SIZE + event->len;
		}
	}

	vlog.verbose("Removing inotify watch");
	inotify_rm_watch(fd, wd);
	close(fd);

	vlog.debug("Leaving thread that watches autotransfer dir");
}

THREAD_FUNC SAutotransferDirWatcher::watchQueueThread(void* _sDirWatcher)
{
	vlog.debug("%s: Starting thread to watch the queue of files to autotransfer", __func__);
	SAutotransferDirWatcher* sDirWatcher = (SAutotransferDirWatcher*) _sDirWatcher;
	while (42 == 42) {
		bool goOn = true;
		while (goOn) {
			char* autotransferFile = NULL;
			MUTEX_LOCK(&processingQueueLocks[sDirWatcher->myThread]);
			if (!sDirWatcher->processingQueue.empty()) {
				autotransferFile = sDirWatcher->processingQueue.front();
				sDirWatcher->processingQueue.pop();
			}
			MUTEX_UNLOCK(&processingQueueLocks[sDirWatcher->myThread]);
			if (autotransferFile) {
				vlog.verbose("%s: Are there more files in processing queue? Yup! Processing '%s'", __func__, autotransferFile);
				sDirWatcher->processFileWithTimeout(autotransferFile, sDirWatcher->processFileTimeoutSeconds);
			} else {
				// Keep going until there are no more files to autotransfer
				vlog.verbose("%s: Are there more files in processing queue? Nope!", __func__);
				goOn = false;
			}
			usleep(100 * 1000);
		}
		MUTEX_LOCK(&filesWaitingConditionLocks[sDirWatcher->myThread]);
		vlog.debug("%s: waiting for somebody to fill the processing queue...", __func__);
		TGVNC_CONDITION_WAIT(&filesWaitingConditions[sDirWatcher->myThread], &filesWaitingConditionLocks[sDirWatcher->myThread]);
		MUTEX_UNLOCK(&filesWaitingConditionLocks[sDirWatcher->myThread]);
	}
	vlog.debug("%s: Exiting thread that watched the queue of files to autotransfer.", __func__);
	THREAD_EXIT(THREAD_NULL);
}

void SAutotransferDirWatcher::addToProcessQueue(char* filename)
{
	bool addedToProcessingQueue;
	MUTEX_LOCK(&processingQueueLocks[myThread]);
	if (!processingQueue.empty() && !strcmp(processingQueue.back(), filename)) {
		// Do not add if it is the same as the last added file (because some inotify events happen one right after the other)
		addedToProcessingQueue = false;
	} else {
		processingQueue.push(filename);
		addedToProcessingQueue = true;
	}
	MUTEX_UNLOCK(&processingQueueLocks[myThread]);

	// We really don't need to lock the queue while we log
	if (addedToProcessingQueue) {
		TGVNC_CONDITION_SEND_SIG(&filesWaitingConditions[myThread]);
		vlog.verbose("%s: Added file '%s' to handling queue. The 'watch queue' thread will take care of it as soon as possible", __func__, filename);
	} else {
		vlog.verbose("%s: File '%s' NOT added to handling queue. because it was just added.", __func__, filename);
	}
}

std::string SAutotransferDirWatcher::getEffectiveUser() const {
	struct passwd* pw = getpwuid(geteuid());
	if (!pw || !pw->pw_name) {
		vlog.error("%s: getpwuid(geteuid()) failed", __func__);
		return "unknown";
	}
	return pw->pw_name;
}

void SAutotransferDirWatcher::logEvent(const char* filename, const char* suffix)
{
	vlog.verbose("%s: logEvent for file '%s' with suffix '%s'", __func__, filename, suffix);

	if (logPathForEvents.empty()) {
		vlog.error("%s: Cannot log event as 'logForPathEvents' is empty. You should definitely tell the dev they didn't work well and that this should be set", __func__);
		return;
	}

	char hostname[HOST_NAME_MAX + 1];
	if (gethostname(hostname, sizeof(hostname)) != 0) {
		strncpy(hostname, "unknown-host", sizeof(hostname));
		hostname[sizeof(hostname) - 1] = '\0';
		vlog.error("%s: Failed to get hostname, using 'unknown-host' (for mp-watch)", __func__);
		syslog(LOG_ERR, "Error: failed to get hostname, using 'unknown-host' (for mp-watch)");
	}

	FILE* handle = fopen(logPathForEvents.c_str(), "a");
	if (!handle) {
		vlog.error("%s: Failed to open log file '%s': %s (mp-watch and the notifications won't work)", __func__, logPathForEvents.c_str(), strerror(errno));
		syslog(LOG_ERR, "Error: failed to open log file '%s' (mp-watch and the notifications won't work)", logPathForEvents.c_str());
		return;
	}

	time_t nowsec = time(nullptr);
	struct tm* now = localtime(&nowsec);

	char line[1024];
	snprintf(line, sizeof(line), "%s %02u.%02u.%04u %02u:%02u:%02u %s %s\n",
		hostname,
		now->tm_mday, now->tm_mon + 1, now->tm_year + 1900,
		now->tm_hour, now->tm_min, now->tm_sec,
		filename,
		suffix ? suffix : "");

	fwrite(line, 1, strlen(line), handle);
	if (ferror(handle)) {
		vlog.error("%s: Failed to write to log file '%s' (mp-watch and the notifications won't work)", __func__, logPathForEvents.c_str());
		syslog(LOG_ERR, "Error: failed to write to log file '%s' (mp-watch and the notifications won't work)", logPathForEvents.c_str());
	} else {
		vlog.verbose("%s: Wrote line '%s' to file '%s' (for the sole purpose of mp-watch showing a notification to the user)", __func__, line, logPathForEvents.c_str());
	}

	fclose(handle);
}

void SAutotransferDirWatcher::setProcessFileTimeout(unsigned int seconds) {
	processFileTimeoutSeconds = seconds;
}

void SAutotransferDirWatcher::processFileWithTimeout(char* filename, unsigned int timeoutSeconds)
{
	if (timeoutSeconds == 0) {
		processFile(filename);
		return;
	}

	pid_t pid = fork();
	if (pid < 0) {
		vlog.error("%s: fork() failed", __func__);
		return;
	}

	if (pid == 0) {
		processFile(filename);
		_exit(0);
	}

	int status = 0;
	auto start = std::chrono::steady_clock::now();

	vlog.info("%s: Calling processFile for file %s with a timeout of %d seconds", __func__, filename, timeoutSeconds);
	while (true) {
		pid_t result = waitpid(pid, &status, WNOHANG);

		if (result == pid)
			return;

		auto elapsed =
			std::chrono::duration_cast<std::chrono::seconds>(
				std::chrono::steady_clock::now() - start).count();

		if (elapsed >= timeoutSeconds) {
			vlog.error("%s: processFile timeout for '%s' (%d seconds)", __func__, filename, timeoutSeconds);
			vlog.debug("%s: Killing child process politely: This will log a shutdown, as all objects will gracefully be destroyed. It will look like a disconnection. Please just ignore.", __func__);
			logEvent(filename, "TIMEOUT");

			// This triggers mp-watch to run immediately. You can ask Amon for more details :)
			// <ao> inotifywait läuft in Dauerschleife, es zu killen triggert mp-watch, sofort was zu tun.
			// <ao> Das ist ein Ersatz für sleep, der sofort stoppt, wenn sich etwas tut.
			system("/usr/bin/pkill inotifywait");

			kill(pid, SIGTERM);
			sleep(2);

			if (waitpid(pid, &status, WNOHANG) != pid) {
				kill(pid, SIGKILL);
				waitpid(pid, &status, 0);
			}

			return;
		}

		sleep(1);
	}
}

#endif /* ifndef WIN32 */
