/* Copyright (C) 2014-2021 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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef WIN32
#include <winsock2.h>
#include <tlhelp32.h>
#define errorNumber WSAGetLastError()
#else
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#define errorNumber errno
#define closesocket close
#endif
#include <time.h>
#include <string>
#include <sys/types.h>
#ifdef __APPLE__
#include "CoreFoundation/CoreFoundation.h"
#endif
#include "tgpro_helpers.h"
#include "tgpro_environment.h"
#include "sound_handler.h"
#include <FL/fl_ask.H>

#include "network/Socket.h"
#include "network/TcpSocket.h"
#include "i18n.h"
#include "CConn.h"
#include "rfb/LogWriter.h"
#include "rdr/MultiOutStream.h"
#include "parameters.h"

#if defined(WIN32) || defined(WIN64)
#include <rfb/Configuration.h>
rfb::BoolParameter paLocalhost("PALocalhost", "Bind Pulseaudio to localhost only", false);
#endif

static void portable_setenv (const char * var_name, const char * value)
{
#ifdef HAVE_SETENV
	setenv(var_name, value, 1);
#else
	int len = strlen(var_name) + strlen(value) + 2;
	char * str = (char *)malloc(len);
	sprintf(str, "%s=%s", var_name, value);
#if defined(WIN32) || defined(WIN64)
	_putenv(str);
#else
	putenv(str);
#endif
	// No free(str) here because putenv() needs allocated memory, which stays allocated (on glibc).
#endif
}

#if defined(WIN32) || defined(WIN64)
static void portable_unsetenv (const char * var_name)
{
#ifdef HAVE_SETENV
	unsetenv(var_name);
#else
	int len = strlen(var_name) + 2;
	char * str = (char *)malloc(len);
	sprintf(str, "%s=", var_name);
	_putenv(str);
	// No free(str) here because putenv() needs allocated memory, which stays allocated (on glibc).
#endif
}
#endif

namespace sound {
#if defined(WIN32) || defined(WIN64)
	int pulsePid = 0;
#else
	pid_t pulsePid = 0;
#endif
	int pulseModuleNo = -1;
	CConn* cc = NULL;
	static rfb::LogWriter vlog("sound");


	void set_conn(CConn* new_cc) {
		cc = new_cc;
	}


	void find_free_socket(int paPortMin, int paPortMax, int * paPort_p) {
#ifdef WIN32
		WORD requiredVersion = MAKEWORD(2,0);
		WSADATA initResult;

		if (WSAStartup(requiredVersion, &initResult))
			throw network::SocketException(_("Failed to initialize Winsock2"), errorNumber);
#endif

		int sockfd;
		if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
			throw network::SocketException(_("Failed to create a network socket"), errorNumber);

		struct sockaddr_in addr;
		srand((unsigned) time(NULL));
		addr.sin_family = AF_INET;
		addr.sin_addr.s_addr = INADDR_ANY;
		int i;
		int bound_sock = -1;
		for (i = 0; i < 10 && bound_sock < 0; i++) {
			double port_range = (paPortMax + 1 - paPortMin);
			double port_min = paPortMin;
			double random_scaler = (double)rand() / ((double)RAND_MAX + 1.0);
			addr.sin_port =	port_min + random_scaler * port_range;
			vlog.info("Socket run try no %i: Port %i", i, addr.sin_port);
			bound_sock = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
		}
		if (i == 10)
			throw network::SocketException(_("Failed to find a free port for PulseAudio"), errorNumber);
		closesocket(sockfd);
		*paPort_p = addr.sin_port;
		char paPortString[10];
		snprintf(paPortString, 10, "%u", *paPort_p);
		portable_setenv("PAPORT", paPortString);
		vlog.debug("Set paPort to %u", *paPort_p);

#if defined(WIN32) || defined(WIN64)
		if (WSACleanup())
			throw network::SocketException(_("Failed to cleanup Winsock2"), errorNumber);
#endif /* defined(WIN32) || defined(WIN64) */
	}

	int load_tcp_module(const char* ip, unsigned port) {
		const size_t pactl_call_len = strlen(pactl_load) + 30;
		char pactl_call[pactl_call_len + 1];
		snprintf(pactl_call, pactl_call_len, pactl_load, ip, port);

		FILE* pactl_output_f = popen(pactl_call, "r");
		if (!pactl_output_f) {
			vlog.info("I was unable to run pactl.");
			return 0;
		}

		char pactl_output_s[32];
		char* pactl_out = fgets(pactl_output_s, 31, pactl_output_f);

		pclose(pactl_output_f);

		if (!pactl_out) {
			vlog.error("pactl did run, but gave no useful output.");
			return 0;
		}
		vlog.info("I told the PulseAudio daemon to listen for sound from %s:%u.", ip, port);

		return atoi(pactl_out);
	}


	void unload_tcp_module(int module_no) {
		const size_t pactl_call_len = strlen(pactl_unload) + 16;
		char pactl_call[pactl_call_len + 1];
		snprintf(pactl_call, pactl_call_len, pactl_unload, module_no);
		vlog.info("Try to unload PulseAudio module #%i", module_no);
		system(pactl_call);
		return;
	}


	static inline bool launch_pulseaudio(const int paPort, network::Socket* const sock) {
#if defined(WIN32) || defined(WIN64)
		/* Check for the pulseaudio dir. */
		const char* pa_dir = tgpro::get_installpath_subdir(pulseaudio_subdir_win);
		if (!pa_dir || !pa_dir[0]) {
			vlog.debug("I could not find pulseaudio subdir.");
			pulsePid = 0;
			char err_msg[256];
			snprintf(err_msg, 256, _("Failed to initialize sound for %s. Couldn't find PulseAudio."), productName.getData());
			char err_title[256];
			snprintf(err_title, 256, _("%s: Sound Error"), productName.getData());
			MessageBox(NULL, err_msg, err_title, MB_ICONWARNING | MB_OK);
			return false;
		}

		vlog.info("I found pulseaudio dir: %s", pa_dir);
		vlog.info("I look for file there: %s", pulseaudio_exec_win);

		/* Check for pulseaudio binary. */
		const size_t pa_exec_cmd_len = strlen(pa_dir) + strlen(pulseaudio_exec_win) + 128;
		char* pa_exec_cmd = (char*)malloc(pa_exec_cmd_len);
		snprintf(pa_exec_cmd, pa_exec_cmd_len, "%s\\%s", pa_dir, pulseaudio_exec_win);

		if (!file_exists(pa_exec_cmd)) {
			vlog.debug("I could not find pulseaudio executable in %s", pa_exec_cmd);
			char err_msg[256];
			snprintf(err_msg, 256, _("Failed to initialize sound for %s. Couldn't find the PulseAudio executable."),
				 productName.getData());
			char err_title[256];
			snprintf(err_title, 256, _("%s: Sound Error"), productName.getData());
			MessageBox(NULL, err_msg, err_title, MB_ICONWARNING | MB_OK);
			free(pa_exec_cmd);
			delete[] pa_dir;
			return false;
		}

		/* Check for default settings file. */
		const size_t conf_path_len = strlen("%s\\default.pa") - 2 + strlen(pa_dir) + 1;
		char conf_path[conf_path_len];
		snprintf(conf_path, conf_path_len, "%s\\default.pa", pa_dir);

		/* Use hardcoded parameters if there is no default settings file. */
		if (!file_exists(conf_path)) {
			const size_t format_len = pa_exec_cmd_len + strlen(pulseaudio_params) + 128;
			char cmd_format[format_len];
			if (paLocalhost) {
				snprintf(cmd_format, format_len, "\"%s\" %s", pa_exec_cmd, pulseaudio_params_localhost);
			} else {
				snprintf(cmd_format, format_len, "\"%s\" %s", pa_exec_cmd, pulseaudio_params);
			}
			const size_t cmd_len = format_len + 128;
			pa_exec_cmd = (char*)realloc(pa_exec_cmd, cmd_len);
			snprintf(pa_exec_cmd, cmd_len, cmd_format, sock->getPeerAddress(), paPort, micSupport ? 1 : 0);
		}

		vlog.info("Start PulseAudio with: <%s>", pa_exec_cmd);

		portable_unsetenv("HOME");
		pulsePid = launch_program(pa_exec_cmd, 0, 0, 0, pa_dir);

		delete[] pa_dir;
		free(pa_exec_cmd);

		if (!pulsePid) {
			char err_msg[256];
			snprintf(err_msg, 256, _("Failed to initialize sound for %s."),
				 productName.getData());
			char err_title[256];
			snprintf(err_title, 256, _("%s: Sound Error"), productName.getData());
			MessageBox(NULL, err_msg, err_title, MB_ICONWARNING | MB_OK);
			return false;
		} else {
			vlog.debug("PulseAudio started with pid %u", pulsePid);
			return true;
		}

#else /* Code for unixoid OSs: */
#ifdef __APPLE__
#ifndef PAEXEDIR
#define PAEXEDIR ./
#endif
#ifndef PAMODDIR
#define PAMODDIR ./Libs/
#endif
#define STRINGIFY(x) XSTRINGIFY(x)
#define XSTRINGIFY(x) #x

		char paCheckCmd[PATH_MAX];

		snprintf(paCheckCmd, PATH_MAX - 1, pulseaudio_check_running_osx, STRINGIFY(PAEXEDIR));
		int pa_not_running = system(paCheckCmd);
#else
		int pa_not_running = system(pulseaudio_check_running);
#endif
		if (pa_not_running) {
			vlog.info("There is no PulseAudio daemon running.");
			char pa_cmd[PATH_MAX];
#ifdef __APPLE__
			snprintf(pa_cmd, PATH_MAX - 1, pulseaudio_start_osx, STRINGIFY(PAEXEDIR), STRINGIFY(PAMODDIR), sock->getPeerAddress(), paPort);
#else
			snprintf(pa_cmd, PATH_MAX - 1, pulseaudio_start_unixoid, sock->getPeerAddress(), paPort);
#endif
			if (!system(pa_cmd)) {
				pulsePid = OWN_UNIX_PULSEAUDIO;
				vlog.info("I started my own PulseAudio daemon.");
			} else {
				vlog.error("I tried to start my own PulseAudio daemon with \"%s\", but it failed.", pa_cmd);
			}
		}
		/* we never fail here, because PA might be controlled elsewhere */
		return true;
#endif /* else defined(WIN32) || defined(WIN64) */
	}


	bool start_sound(int paPort) {
		if (pulsePid)
			return true;
		if (!cc) {
			vlog.error("ERROR: Could not start sound because I got no CConn object.");
			return false;
		}

		return launch_pulseaudio(paPort, cc->getSock());
	}


	void stop_sound() {
		if (!pulsePid)
			return;
		if (pulseModuleNo > 0)
			unload_tcp_module(pulseModuleNo);
		if (pulsePid == SYSTEM_PULSEAUDIO)
			return;
#if defined(WIN32) || defined(WIN64)
		vlog.debug("Killing PulseAudio process with pid %u", pulsePid);
		UINT ecode = 0;
		HANDLE ps = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE,
					FALSE, pulsePid);
		pulsePid = 0;
		TerminateProcess(ps, ecode);
		CloseHandle(ps);
#else
#if defined(__APPLE__)
		vlog.debug("Killing all pulseaudio processes (because we started the process).");
		system("killall pulseaudio");
#endif
#endif /* defined(WIN32) || defined(WIN64) */
	}


	void kill_all_pulseaudio_processes() {
#if defined(WIN32) || defined(WIN64)
		HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);
		PROCESSENTRY32 pEntry;
		pEntry.dwSize = sizeof(pEntry);
		BOOL hRes = Process32First(hSnapShot, &pEntry);
		while (hRes) {
			if (strcmp(pEntry.szExeFile, "pulseaudio.exe") == 0) {
				vlog.debug("There is an old pulseaudio process running! I try to stop it.");
				HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, 0,
							      (DWORD)pEntry.th32ProcessID);
				if (hProcess != NULL) {
					TerminateProcess(hProcess, 9);
					if (WaitForSingleObject(hProcess, 5000) == WAIT_TIMEOUT)
						vlog.error("I could not stop the old pulseaudio process (in 5 seconds).");
					else
						vlog.debug("I did stop the old pulseaudio process.");
					CloseHandle(hProcess);
					Sleep(1000); /* A pause is needed, otherwise new pulseaudio would not start. */
				}
			}
			hRes = Process32Next(hSnapShot, &pEntry);
		}
		CloseHandle(hSnapShot);
#endif /* defined(WIN32) || defined(WIN64) */
	}
}
