/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/win/launcher_win.h"

#include "core/crash_reports.h"
#include "core/update_checker.h"

#include <windows.h>
#include <shellapi.h>
#include <VersionHelpers.h>

#include <crtdbg.h>
#include <cstdlib>

namespace Platform {
namespace {

// Turn a CRT/STL assertion (or pure-virtual / invalid-parameter) into a real,
// crash-reported failure instead of a modal Abort/Retry/Ignore dialog that
// would block a headless test agent forever.
[[noreturn]] void TestAgentReportCrash(
		const char *kind,
		const QString &message) {
	if (!message.isEmpty()) {
		CrashReports::SetAnnotation("CrtAssert", message);
	}
	// Be loud on stderr too (captured when the launch redirects it to a file).
	fprintf(stderr,
		"\n[testagent] %s: %s\n",
		kind,
		message.toLocal8Bit().constData());
	fflush(stderr);

	// Crashes with an access violation -> caught by the crash reporter, dumped
	// to tdata/working + tdata/dumps, and the process exits non-zero. No dialog.
	Unexpected("Test agent: CRT/STL assertion violation.");
}

#ifdef _DEBUG
int TestAgentReportHook(int reportType, char *message, int *returnValue) {
	if (returnValue) {
		*returnValue = 0; // Do not break into the (absent) debugger.
	}
	if (reportType == _CRT_ASSERT || reportType == _CRT_ERROR) {
		TestAgentReportCrash(
			"assert",
			QString::fromLocal8Bit(message ? message : ""));
	} else if (message) {
		fputs(message, stderr);
		fflush(stderr);
	}
	return TRUE; // Handled -> suppress the default message box.
}

int TestAgentReportHookW(int reportType, wchar_t *message, int *returnValue) {
	if (returnValue) {
		*returnValue = 0;
	}
	if (reportType == _CRT_ASSERT || reportType == _CRT_ERROR) {
		TestAgentReportCrash(
			"assert",
			QString::fromWCharArray(message ? message : L""));
	} else if (message) {
		fputws(message, stderr);
		fflush(stderr);
	}
	return TRUE;
}
#endif // _DEBUG

void InstallTestAgentCrashHandling() {
	// No Windows Error Reporting / general-protection / abort() message boxes —
	// each would block the test agent waiting on a button it cannot press.
	SetErrorMode(SEM_FAILCRITICALERRORS
		| SEM_NOGPFAULTERRORBOX
		| SEM_NOOPENFILEERRORBOX);
	_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);

	// Pure-virtual call / invalid CRT parameter -> reported crash, not a dialog.
	_set_purecall_handler([] {
		TestAgentReportCrash("purecall", u"Pure virtual function call"_q);
	});
	_set_invalid_parameter_handler([](
			const wchar_t *expression,
			const wchar_t *,
			const wchar_t *,
			unsigned int,
			uintptr_t) {
		TestAgentReportCrash(
			"invalid-parameter",
			QString::fromWCharArray(
				expression ? expression : L"CRT invalid parameter"));
	});

#ifdef _DEBUG
	// assert()/_ASSERTE and STL bounds/iterator checks (_STL_VERIFY) report via
	// _CrtDbgReport[W]; route them to stderr (never a window) and convert any
	// assertion into a reported crash through the hooks above.
	for (const auto type : { _CRT_WARN, _CRT_ERROR, _CRT_ASSERT }) {
		_CrtSetReportMode(type, _CRTDBG_MODE_FILE);
		_CrtSetReportFile(type, _CRTDBG_FILE_STDERR);
	}
	_CrtSetReportHook2(_CRT_RPTHOOK_INSTALL, TestAgentReportHook);
	_CrtSetReportHookW2(_CRT_RPTHOOK_INSTALL, TestAgentReportHookW);
#endif // _DEBUG
}

} // namespace

Launcher::Launcher(int argc, char *argv[])
: Core::Launcher(argc, argv) {
}

void Launcher::initHook() {
	if (cTestAgent()) {
		InstallTestAgentCrashHandling();
	}
}

std::optional<QStringList> Launcher::readArgumentsHook(
		int argc,
		char *argv[]) const {
	auto count = 0;
	if (const auto list = CommandLineToArgvW(GetCommandLine(), &count)) {
		const auto guard = gsl::finally([&] { LocalFree(list); });
		if (count > 0) {
			auto result = QStringList();
			result.reserve(count);
			for (auto i = 0; i != count; ++i) {
				result.push_back(QString::fromWCharArray(list[i]));
			}
			return result;
		}
	}
	return std::nullopt;
}

bool Launcher::launchUpdater(UpdaterLaunch action) {
	if (cExeName().isEmpty()) {
		return false;
	}

	const auto operation = (action == UpdaterLaunch::JustRelaunch)
		? QString()
		: (cWriteProtected()
			? u"runas"_q
			: QString());
	const auto binaryPath = (action == UpdaterLaunch::JustRelaunch)
		? (cExeDir() + cExeName())
		: (cWriteProtected()
			? (cWorkingDir() + u"tupdates/temp/Updater.exe"_q)
			: (cExeDir() + u"Updater.exe"_q));

	auto argumentsList = QStringList();
	const auto pushArgument = [&](const QString &argument) {
		argumentsList.push_back(argument.trimmed());
	};
	if (cLaunchMode() == LaunchModeAutoStart) {
		pushArgument(u"-autostart"_q);
	}
	if (Logs::DebugEnabled()) {
		pushArgument(u"-debug"_q);
	}
	if (cStartInTray()) {
		pushArgument(u"-startintray"_q);
	}
	if (customWorkingDir()) {
		pushArgument(u"-workdir"_q);
		pushArgument('"' + cWorkingDir() + '"');
	}
	if (cDataFile() != u"data"_q) {
		pushArgument(u"-key"_q);
		pushArgument('"' + cDataFile() + '"');
	}

	if (action == UpdaterLaunch::JustRelaunch) {
		pushArgument(u"-noupdate"_q);
		if (cRestartingToSettings()) {
			pushArgument(u"-tosettings"_q);
		}
	} else {
		pushArgument(u"-update"_q);
		pushArgument(u"-exename"_q);
		pushArgument('"' + cExeName() + '"');
		if (cWriteProtected()) {
			pushArgument(u"-writeprotected"_q);
			pushArgument('"' + cExeDir() + '"');
		}
	}
	return launch(operation, binaryPath, argumentsList);
}

bool Launcher::launch(
		const QString &operation,
		const QString &binaryPath,
		const QStringList &argumentsList) {
	const auto convertPath = [](const QString &path) {
		return QDir::toNativeSeparators(path).toStdWString();
	};
	const auto nativeBinaryPath = convertPath(binaryPath);
	const auto nativeWorkingDir = convertPath(cWorkingDir());
	const auto arguments = argumentsList.join(' ');

	DEBUG_LOG(("Application Info: executing %1 %2"
		).arg(binaryPath
		).arg(arguments
		));

	Logs::closeMain();
	CrashReports::Finish();

	const auto hwnd = HWND(0);
	const auto result = ShellExecute(
		hwnd,
		operation.isEmpty() ? nullptr : operation.toStdWString().c_str(),
		nativeBinaryPath.c_str(),
		arguments.toStdWString().c_str(),
		nativeWorkingDir.empty() ? nullptr : nativeWorkingDir.c_str(),
		SW_SHOWNORMAL);
	if (int64(result) < 32) {
		DEBUG_LOG(("Application Error: failed to execute %1, working directory: '%2', result: %3"
			).arg(binaryPath
			).arg(cWorkingDir()
			).arg(int64(result)
			));
		return false;
	}
	return true;
}

} // namespace Platform
