// -*- c++ -*-
// Distributed under the BSD 2-Clause License.
// See accompanying file LICENSE for details.
#include "execute.h"

#include "ctor.h"
#include "pointerlist.h"

#if !defined(_WIN32)
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <spawn.h>
extern char **environ;
#else
#define WINDOWS_LEAN_AND_MEAN
#include <windows.h>
#undef max
#include <thread>
#endif

#include <iostream>
#include <cstring>
#include <vector>
#include <deque>
#include <filesystem>

/*
https://blog.famzah.net/2009/11/20/a-much-faster-popen-and-system-implementation-for-linux/
https://github.com/famzah/popen-noshell/commit/1f9eaf4eeef348d1efe0f3c7fe8ab670653cfbb1
https://blog.famzah.net/2018/12/19/posix_spawn-performance-benchmarks-and-usage-examples/
https://stackoverflow.com/questions/4259629/what-is-the-difference-between-fork-and-vfork/5207945#5207945
 */


namespace
{
#if !defined(_WIN32)
int parent_waitpid(pid_t pid)
{
	int status{};

	auto rc_pid = waitpid(pid, &status, 0);

	if(rc_pid > 0)
	{
		if(WIFEXITED(status))
		{
			// Child exited with normally
			return WEXITSTATUS(status);
		}
		if(WIFSIGNALED(status))
		{
			// Child exited via signal (segfault, abort, ...)
			std::cerr << strsignal(status) << '\n';
			return WTERMSIG(status);
		}
	}
	else
	{ // No PID returned, this is an error
		if(errno == ECHILD)
		{
			// No children exist.
			return 1;
		}
		else
		{
			// Unexpected error.
			abort();
		}
	}

	// Should never happen...
	return 1;
}
#endif //_WIN32

#if defined(_WIN32)
std::string getLastErrorAsString()
{
	DWORD errorMessageID = ::GetLastError();
	if(errorMessageID == 0)
	{
		return {};
	}

	LPSTR message_buffer{};
	auto size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER |
	                           FORMAT_MESSAGE_FROM_SYSTEM |
	                           FORMAT_MESSAGE_IGNORE_INSERTS,
	                           nullptr,
	                           errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
	                           (LPSTR)&message_buffer, 0, nullptr);

	std::string message(message_buffer, size);
	LocalFree(message_buffer);
	return message;
}

int moveSelf(const ctor::settings& settings)
{
	int cnt{0};
	char source[MAX_PATH];
	HMODULE module = GetModuleHandle(0);
	GetModuleFileNameA(module, source, MAX_PATH);

	while(true)
	{
		if(cnt > 10) // If we need to try more than 10 times something is wrong
		{
			return 1;
		}

		std::filesystem::path tmp_file = settings.builddir;
		tmp_file /= "tmp";
		std::string target = tmp_file.string() + "-" + std::to_string(cnt);
		if(MoveFileA(source, target.data()))
		{
			break; // success
		}

		auto err = GetLastError();
		if(err == ERROR_ALREADY_EXISTS)
		{
			if(DeleteFileA(target.data()))
			{
				continue; // Try again
			}

			err = GetLastError();
			if(err != ERROR_ACCESS_DENIED)
			{
				std::cerr << "Could not delete file\n";
				return err;
			}

			cnt++;
			continue; // Increment and try again
		}
		else
		{
			std::cerr << "Could not move file\n";
			return err;
		}
	}
	return 0;
}
#endif // _WIN32
} // namespace ::

int execute(const ctor::settings& settings,
            const std::string& command,
            const std::vector<std::string>& args,
            const std::map<std::string, std::string>& env,
            [[maybe_unused]] bool terminate)
{
	std::vector<const char*> argv;
	argv.push_back(command.data());
	for(const auto& arg : args)
	{
		argv.push_back(arg.data());
	}
	argv.push_back(nullptr);

	std::string cmd;
	for(const auto& arg : argv)
	{
		if(arg == nullptr)
		{
			break;
		}
		if(!cmd.empty())
		{
			cmd += " ";
		}
		cmd += arg;
	}

	if(settings.verbose > 0)
	{
		std::cout << cmd << std::endl;
	}

#if !defined(_WIN32)

#if 1
	auto pid = vfork();
	if(pid == 0)
	{
		EnvMap envmap(environ);
		for(const auto& [key, value] : env)
		{
			envmap.insert(key + "=" + value);
		}
		if(settings.dry_run)
		{
			_exit(0);
		}
		auto [_, envv] = envmap.get();
		execve(command.data(), const_cast<char* const *>(argv.data()),
		       const_cast<char* const *>(envv));
		std::cout << "Could not execute " << command << ": " <<
			strerror(errno) << "\n";
		_exit(1); // execve only returns if an error occurred
	}
	return parent_waitpid(pid);
#elif 0
	pid_t pid{};
	EnvMap envmap(environ);
	for(const auto& [key, value] : env)
	{
		envmap.insert(key + "=" + value);
	}

	auto [_, envv] = envmap.get();
	if(posix_spawn(&pid, command.data(), nullptr, nullptr,
	               (char**)argv.data(), const_cast<char* const *>(envv)))
	{
		return 1;
	}
	return parent_waitpid(pid);
#else
	(void)parent_waitpid;
	return system(cmd.data());
#endif

#else // _WIN32
	if(terminate)
	{
		auto ret = moveSelf(settings);
		if(ret != 0)
		{
			return ret;
		}
	}

	auto env_strings = GetEnvironmentStrings();
	EnvMap envmap(env_strings);
	FreeEnvironmentStrings(env_strings);
	for(const auto& [key, value] : env)
	{
		envmap.insert(key + "=" + value);
	}

	// TODO: Use SetDllDirectory(...) to set DLL search directory?

	SECURITY_ATTRIBUTES security_attr;
	// Set the bInheritHandle flag so pipe handles are inherited.
	security_attr.nLength = sizeof(SECURITY_ATTRIBUTES);
	security_attr.bInheritHandle = TRUE;
	security_attr.lpSecurityDescriptor = nullptr;

	HANDLE stream_in_read{};
	HANDLE stream_in_write{};
	HANDLE stream_out_read{};
	HANDLE stream_out_write{};

	if(!CreatePipe(&stream_out_read, &stream_out_write, &security_attr, 0))
	{
		std::cout << "Error CreatePipe (out): " << getLastErrorAsString() << "\n";
	}

	if(!SetHandleInformation(stream_out_read, HANDLE_FLAG_INHERIT, 0))
	{
		std::cout << "Error - SetHandleInformation (out): " << getLastErrorAsString() << "\n";
	}

	// Create a pipe for the child process's STDIN.
	if(!CreatePipe(&stream_in_read, &stream_in_write, &security_attr, 0))
	{
		std::cout << "Error CreatePipe (in): " << getLastErrorAsString() << "\n";
	}

	// Ensure the write handle to the pipe for STDIN is not inherited.
	if(!SetHandleInformation(stream_in_write, HANDLE_FLAG_INHERIT, 0))
	{
		std::cout << "Error - SetHandleInformation (in): " << getLastErrorAsString() << "\n";
	}

	STARTUPINFO si{};
	si.hStdInput = GetStdHandle(((DWORD)-10));//STD_INPUT_HANDLE
	si.hStdOutput = stream_out_write;
	si.hStdError = stream_out_write;
	si.dwFlags |= STARTF_USESTDHANDLES;

	PROCESS_INFORMATION pi{};

	si.cb = sizeof(si);

	if(!CreateProcess(nullptr,           // lpApplicationName
	                  (char*)cmd.data(), // lpCommandLine
	                  nullptr,           // lpProcessAttributes
	                  nullptr,           // lpThreadAttributes
	                  TRUE,              // bInheritHandles
	                  INHERIT_PARENT_AFFINITY |
	                  0x00000200 | // CREATE_NEW_PROCESS_GROUP
	                  0,                 // dwCreationFlags
	                  envmap.stringify().data(),    // lpEnvironment
	                  nullptr,           // lpCurrentDirectory
	                  &si,               // lpStartupInfo
	                  &pi))              // lpProcessInformation
	{
		std::cout << "Could not execute " << command << ": " <<
			getLastErrorAsString() << "\n";
		return 1;
	}

	int ignore_lines{};
	std::filesystem::path cmd_path{command};
	if(cmd_path.filename() == "cl.exe")
	{
		// Ignore first line, avoiding the annoying msvc compiler of printing the filename
		// of the file that is being compiled
		ignore_lines = 1;
	}

	std::atomic<bool> running{true};
	auto parent_waitpid = std::thread([&](){
		WaitForSingleObject(pi.hProcess, INFINITE);
		CloseHandle(stream_out_write);
		CloseHandle(stream_in_read);
		running.store(false);
	});

	CHAR buf[1024];
	while(running.load())
	{
		if(WaitForSingleObject(stream_out_read, 0) != WAIT_OBJECT_0)
		{
			Sleep(1);
			continue;
		}
		DWORD read_bytes{};
		auto res = ReadFile(stream_out_read, buf, sizeof(buf), &read_bytes, nullptr);
		if(res != TRUE || read_bytes == 0)
		{
			break;
		}
		std::string str;
		str.append(buf, read_bytes);
		if(ignore_lines == 0)
		{
			std::cout << str << std::flush;
		}
		if(str.find('\n') != std::string::npos)
		{
			ignore_lines = std::max(0, ignore_lines - 1);
		}
	}

	DWORD exit_code{};
	GetExitCodeProcess(pi.hProcess, &exit_code);
	parent_waitpid.join();

	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);

	return exit_code;
#endif // _WIN32

	return 1;
}