// Copyright 2005-2017 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "mumble_pch.hpp"

#include "Overlay.h"

#include "User.h"
#include "Channel.h"
#include "OverlayConfig.h"
#include "MainWindow.h"
#include "Global.h"
#include "MumbleApplication.h"

#include "Overlay_win.h"

#include "../../overlay/overlay_exe/overlay_exe.h"

// Used by the overlay to detect whether we injected into ourselves.
//
// A similar declaration can be found in mumble_exe's Overlay.cpp,
// for the overlay's self-detection checks to continue working in a
// mumble_app.dll world.
extern "C" __declspec(dllexport) void mumbleSelfDetection() {};

// Determine if the current Mumble client is able to host
// x64 programs.
//
// If we're on x86, we use use the IsWoW64Process function
// to determine this.  If we're on x64, we already know we're
// capable, so we simply return TRUE.
static bool canRun64BitPrograms() {
#if defined(_M_X64)
	return TRUE;
#elif defined(_M_IX86)
	typedef BOOL (WINAPI *IsWow64ProcessPtr)(HANDLE, BOOL *);
	IsWow64ProcessPtr wow64check = (IsWow64ProcessPtr)GetProcAddress(GetModuleHandle(L"kernel32"), "IsWow64Process");
	if (wow64check) {
		BOOL isWoW64 = FALSE;
		wow64check(GetCurrentProcess(), &isWoW64);
		return isWoW64;
	}
	return FALSE;
#endif
}

OverlayPrivateWin::OverlayPrivateWin(QObject *p) : OverlayPrivate(p) {
	m_active = false;

	// Acquire a handle to ourselves and duplicate it. We duplicate it because
	// want it to be inheritable by our helper processes, and the handle returned
	// by GetCurrentProcess is not inheritable. Duplicating it makes it inheritable.
	// This allows our helper processes to access the handle.
	//
	// The helper processes need a handle to us, their parent, to be able to listen
	// detect when our process dies.
	//
	// The value of the handle is passed as an argument to the helper processes via
	// the command line as a number. The HANDLE type in Windows is typedef'd to LPVOID,
	// but for handles that are supposed to be shared between processes (like a process
	// handle that we are using), only the lower 32-bits of the HANDLE are considered:
	//
	//   "When sharing a handle between 32-bit and 64-bit applications, only the lower
	//    32 bits are significant [...]"
	//
	// from https://msdn.microsoft.com/en-us/library/aa384203.aspx
	HANDLE curproc = GetCurrentProcess();
	if (!DuplicateHandle(curproc, curproc, curproc, &m_mumble_handle,
		                0, TRUE, DUPLICATE_SAME_ACCESS)) {
		qFatal("OverlayPrivateWin: unable to duplicate handle to the Mumble process.");
		return;
	}

	m_helper_exe_path = QString::fromLatin1("%1/mumble_ol_helper.exe").arg(MumbleApplication::instance()->applicationVersionRootPath());
	m_helper_exe_args << QString::number(OVERLAY_MAGIC_NUMBER)
	                  << QString::number(reinterpret_cast<quintptr>(m_mumble_handle));
	m_helper_process = new QProcess(this);

	connect(m_helper_process, SIGNAL(started()),
	        this, SLOT(onHelperProcessStarted()));

	connect(m_helper_process, SIGNAL(error(QProcess::ProcessError)),
	        this, SLOT(onHelperProcessError(QProcess::ProcessError)));

	connect(m_helper_process, SIGNAL(finished(int, QProcess::ExitStatus)),
	        this, SLOT(onHelperProcessExited(int, QProcess::ExitStatus)));

	m_helper_restart_timer = new QTimer(this);
	m_helper_restart_timer->setSingleShot(true);
	connect(m_helper_restart_timer, SIGNAL(timeout()), this, SLOT(onDelayedRestartTimerTriggered()));

	if (!g.s.bOverlayWinHelperX86Enable) {
		qWarning("OverlayPrivateWin: mumble_ol_helper.exe (32-bit overlay helper) disabled via 'overlay_win/enable_x86_helper' config option.");
		m_helper_enabled = false;
	}

	m_helper64_exe_path = QString::fromLatin1("%1/mumble_ol_helper_x64.exe").arg(MumbleApplication::instance()->applicationVersionRootPath());
	m_helper64_exe_args = m_helper_exe_args;
	m_helper64_process = new QProcess(this);

	connect(m_helper64_process, SIGNAL(started()),
	        this, SLOT(onHelperProcessStarted()));

	connect(m_helper64_process, SIGNAL(error(QProcess::ProcessError)),
	        this, SLOT(onHelperProcessError(QProcess::ProcessError)));

	connect(m_helper64_process, SIGNAL(finished(int, QProcess::ExitStatus)),
	        this, SLOT(onHelperProcessExited(int, QProcess::ExitStatus)));

	m_helper64_restart_timer = new QTimer(this);
	m_helper64_restart_timer->setSingleShot(true);
	connect(m_helper64_restart_timer, SIGNAL(timeout()), this, SLOT(onDelayedRestartTimerTriggered()));

	if (!canRun64BitPrograms()) {
		qWarning("OverlayPrivateWin: mumble_ol_helper_x64.exe (64-bit overlay helper) disabled because the host is not x64 capable.");
		m_helper64_enabled = false;
	} else if (!g.s.bOverlayWinHelperX64Enable) {
		qWarning("OverlayPrivateWin: mumble_ol_helper_x64.exe (64-bit overlay helper) disabled via 'overlay_win/enable_x64_helper' config option.");
		m_helper64_enabled = false;
	}
}

OverlayPrivateWin::~OverlayPrivateWin() {
	m_active = false;

	if (!CloseHandle(m_mumble_handle)) {
		qFatal("OverlayPrivateWin: unable to close Mumble process handle.");
		return;
	}
}

void OverlayPrivateWin::startHelper(QProcess *helper) {
	if (helper->state() == QProcess::NotRunning) {
		if (helper == m_helper_process) {
			helper->start(m_helper_exe_path, m_helper_exe_args);
			m_helper_start_time.restart();
		} else if (helper == m_helper64_process) {
			helper->start(m_helper64_exe_path, m_helper64_exe_args);
			m_helper64_start_time.restart();
		} else {
			qFatal("OverlayPrivateWin: invalid helper passed to startHelper().");
		}
	} else {
		qWarning("OverlayPrivateWin: startHelper() called while process is already running. skipping.");
	}
}

void OverlayPrivateWin::setActive(bool active) {
	if (m_active != active) {
		m_active = active;

		if (m_active) {
			if (m_helper_enabled) {
				startHelper(m_helper_process);
			}
			if (m_helper64_enabled) {
				startHelper(m_helper64_process);
			}
		} else {
			if (m_helper_enabled) {
				m_helper_process->terminate();
			}
			if (m_helper64_enabled) {
				m_helper64_process->terminate();
			}
		}
	}
}

static const char *exitStatusString(QProcess::ExitStatus exitStatus) {
	switch (exitStatus) {
		case QProcess::NormalExit:
			return "normal exit";
		case QProcess::CrashExit:
			return "crash";
	}

	return "unknown";
}

static const char *processErrorString(QProcess::ProcessError processError) {
	switch (processError) {
		case QProcess::FailedToStart:
			return "process failed to start";
		case QProcess::Crashed:
			return "process crashed";
		case QProcess::Timedout:
			return "process wait operation timed out";
		case QProcess::WriteError:
			return "an error occurred when attempting to write to the process";
		case QProcess::ReadError:
			return "an error occurred when attempting to read from the process";
	}

	return "unknown";
}

void OverlayPrivateWin::onHelperProcessStarted() {
	QProcess *helper = qobject_cast<QProcess *>(sender());
	QString path;
	if (helper == m_helper_process) {
		path = m_helper_exe_path;
	} else if (helper == m_helper64_process) {
		path = m_helper64_exe_path;
	} else {
		qFatal("OverlayPrivateWin: unknown QProcess found in onHelperProcessStarted().");
	}

	PROCESS_INFORMATION *pi = helper->pid();
	qWarning("OverlayPrivateWin: overlay helper process '%s' started with PID %llu.",
	         qPrintable(path), static_cast<unsigned long long>(pi->dwProcessId));
}

void OverlayPrivateWin::onHelperProcessError(QProcess::ProcessError processError) {
	QProcess *helper = qobject_cast<QProcess *>(sender());
	QString path;
	if (helper == m_helper_process) {
		path = m_helper_exe_path;
	} else if (helper == m_helper64_process) {
		path = m_helper64_exe_path;
	} else {
		qFatal("OverlayPrivateWin: unknown QProcess found in onHelperProcessError().");
	}

	qWarning("OverlayPrivateWin: an error occured for overlay helper process '%s': %s",
	         qPrintable(path),
	         processErrorString(processError));
}

void OverlayPrivateWin::onHelperProcessExited(int exitCode, QProcess::ExitStatus exitStatus) {
	QProcess *helper = qobject_cast<QProcess *>(sender());

	QString path;
	qint64 elapsedMsec = 0;
	QTimer *restartTimer = NULL;
	if (helper == m_helper_process) {
		path = m_helper_exe_path;
		elapsedMsec = m_helper_start_time.elapsed();
		restartTimer = m_helper_restart_timer;
	} else if (helper == m_helper64_process) {
		path = m_helper64_exe_path;
		elapsedMsec = m_helper64_start_time.elapsed();
		restartTimer = m_helper64_restart_timer;
	} else {
		qFatal("OverlayPrivateWin: unknown QProcess found in onHelperProcessExited().");
	}

	const char *helperErrString = OverlayHelperErrorToString(static_cast<OverlayHelperError>(exitCode));
	qWarning("OverlayPrivateWin: overlay helper process '%s' exited (%s) with status code %s.",
	         qPrintable(path),
	         exitStatusString(exitStatus),
	         helperErrString ? helperErrString : qPrintable(QString::number(exitCode)));

	// If the helper process exited while we're in 'active'
	// mode, restart it.
	if (m_active) {
		// If the helper was only recently started, be
		// a little more patient with restarting it.
		// We could be hitting a crash bug in the helper,
		// and we don't want to do too much harm in that
		// case by spawning thousands of processes.
		qint64 cooldownMsec = (qint64) g.s.iOverlayWinHelperRestartCooldownMsec;
		if (elapsedMsec < cooldownMsec) {
			qint64 delayMsec = cooldownMsec - elapsedMsec;
			qWarning("OverlayPrivateWin: waiting %llu seconds until restarting helper process '%s'. last restart was %llu seconds ago.",
				(unsigned long long) delayMsec/1000ULL,
				qPrintable(path),
				(unsigned long long) elapsedMsec/1000ULL);
			if (!restartTimer->isActive()) {
				restartTimer->start(delayMsec);
			}
		} else {
			startHelper(helper);
		}
	}
}

void OverlayPrivateWin::onDelayedRestartTimerTriggered() {
	if (!m_active) {
		return;
	}

	QTimer *timer = qobject_cast<QTimer *>(sender());

	QProcess *helper = NULL;
	if (timer == m_helper_restart_timer) {
		helper = m_helper_process;
	} else if (timer == m_helper64_restart_timer) {
		helper = m_helper64_process;
	} else {
		qFatal("OverlayPrivateWin: unknown timer found in onDelayedRestartTimerTriggered().");
	}

	if (helper->state() == QProcess::NotRunning) {
		startHelper(helper);
	}
}

void Overlay::platformInit() {
	d = new OverlayPrivateWin(this);
}

void Overlay::setActive(bool act) {
	static_cast<OverlayPrivateWin *>(d)->setActive(act);
}

bool OverlayConfig::supportsInstallableOverlay() {
	return false;
}

bool OverlayConfig::isInstalled() {
	return true;
}

bool OverlayConfig::needsUpgrade() {
	return false;
}

bool OverlayConfig::installFiles() {
	return false;
}

bool OverlayConfig::uninstallFiles() {
	return false;
}
