/*
    PIP - Platform Independent Primitives
    Process
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pitime.h"
#ifndef MICRO_PIP

#  include "piincludes_p.h"
#  include "piliterals_bytes.h"
#  include "piprocess.h"
#  include "pitranslator.h"
#  ifndef WINDOWS
#	include <csignal>
#	include <sys/wait.h>
#	include <fcntl.h>
#  endif
#  ifdef MAC_OS
#	include <crt_externs.h>
#  endif


//! \class PIProcess piprocess.h
//! \~\details
//! \~english
//! This class able to start external executables, watch for them,
//! grab output and exit code.
//!
//! \~russian
//!
//!
//! \~english \section PIProcess_sec0 Synopsis
//! \~russian \section PIProcess_sec0 Краткий обзор
//! \~english
//! External executable can be started with control or fully independent
//! from application.\n
//! Start with control allow you to wait for finish, grab output
//! and terminate it at any time.\n
//! You can change working directory and environment of executable.\n
//!
//! \~russian
//!
//!
//! \~english \section PIProcess_sec1 Usage
//! \~russian \section PIProcess_sec1 Использование
//! \~english
//!
//!
//! \~russian
//!


namespace {
enum PipeDirection {
	PipeRead,
	PipeWrite,
	PipeLast = PipeWrite
};

enum StdFile {
	StdIn,
	StdOut,
	StdErr,
	StdLast = StdErr
};

constexpr int PipesDirections = PipeLast + 1;
constexpr int StdFileCount    = StdLast + 1;

#  ifdef WINDOWS
using PipeHandleType = HANDLE;
using SizeType       = DWORD;
#  else
using SizeType       = ssize_t;
using PipeHandleType = int;
#  endif

#  ifdef WINDOWS
PIString convertWindowsCmd(PIStringList sl) {
	if (sl.isNotEmpty()) {
		sl[0].replaceAll('/', '\\');
		sl[0].quote();
	}
	return sl.join(' ');
}
#  else
char * const * convertToCharArrays(const PIStringList & sl) {
	char ** cc = new char *[sl.size() + 1];
	for (int i = 0; i < sl.size_s(); ++i) {
		cc[i] = const_cast<char *>(sl[i].data());
	}
	cc[sl.size()] = 0;
	return cc;
}
#  endif

} // namespace


PRIVATE_DEFINITION_START(PIProcess)

#  ifdef WINDOWS
	PROCESS_INFORMATION pi;
#  else
	pid_t pid;
#  endif
	PipeHandleType pipes[StdFileCount][PipesDirections];
	bool grab[StdFileCount];


	void forEachPipe(std::function<void(PipeHandleType &)> func) {
		for (int i = 0; i < StdFileCount; ++i) {
			for (int j = 0; j < PipesDirections; ++j) {
				func(pipes[i][j]);
			}
		}
	}


	void initGrab() {
		for (int i = 0; i < StdFileCount; ++i) {
			grab[i] = false;
		}
	}


	bool createPipe(StdFile pipe_type) {
		const int pt = pipe_type;
#  ifdef WINDOWS
		SECURITY_ATTRIBUTES saAttr;
		saAttr.nLength              = sizeof(SECURITY_ATTRIBUTES);
		saAttr.bInheritHandle       = TRUE;
		saAttr.lpSecurityDescriptor = NULL;
		if (!CreatePipe(&(pipes[pt][PipeRead]), &(pipes[pt][PipeWrite]), &saAttr, 0)) return false;
		return true;
#  else
		int ret = pipe(pipes[pt]);
		return ret != -1;
#  endif
		return false;
	}


	bool createPipes() {
		for (int i = 0; i < StdFileCount; ++i) {
			if (grab[i]) {
				if (!createPipe(static_cast<StdFile>(i))) {
					piCout << "CreatePipe failed";
#  ifdef WINDOWS
					piCout << GetLastError();
#  endif
					return false;
				}
			}
		}
#  ifdef WINDOWS
		if (grab[StdIn]) SetHandleInformation(pipes[StdIn][PipeWrite], HANDLE_FLAG_INHERIT, 0);
		if (grab[StdOut]) SetHandleInformation(pipes[StdOut][PipeRead], HANDLE_FLAG_INHERIT, 0);
		if (grab[StdErr]) SetHandleInformation(pipes[StdErr][PipeRead], HANDLE_FLAG_INHERIT, 0);
#  endif
		return true;
	}


	void closePipe(StdFile pipe_type, PipeDirection direction) {
		closePipe(pipes[pipe_type][direction]);
	}


	void closePipe(PipeHandleType & hpipe) {
#  ifdef WINDOWS
		if (hpipe != 0) {
			CloseHandle(hpipe);
			hpipe = 0;
		}
#  else
		if (hpipe != -1) {
			::close(hpipe);
			hpipe = -1;
		}
#  endif
	}


	void closeAllPipes() {
		forEachPipe([this](PipeHandleType & hpipe) { closePipe(hpipe); });
	}


	PIByteArray readPipe(StdFile pipe_type) {
		if (!grab[pipe_type]) return {};
		constexpr size_t read_buffer_size = 4_KiB;
		PIByteArray read_buffer;
		read_buffer.resize(read_buffer_size);
		SizeType bytes_read = 0;
		size_t offset       = 0;
		while (1) {
#  ifdef WINDOWS
			DWORD available = 0;
			bytes_read      = 0;
			PeekNamedPipe(pipes[pipe_type][PipeRead], nullptr, 0, nullptr, &available, nullptr);
			if (available > 0) {
				BOOL ok = ReadFile(pipes[pipe_type][PipeRead],
				                   read_buffer.data(offset),
				                   piMini(available, read_buffer.size() - offset),
				                   &bytes_read,
				                   nullptr);
				if (!ok) bytes_read = 0;
			}
#  else
			bytes_read = ::read(pipes[pipe_type][PipeRead], read_buffer.data(offset), read_buffer.size() - offset);
#  endif
			if (bytes_read > 0) {
				offset += bytes_read;
				read_buffer.resize(offset + read_buffer_size);
			} else {
				read_buffer.resize(offset);
				break;
			}
		}
		return read_buffer;
	}


	bool writePipe(const PIByteArray & data) {
		SizeType sz = 0;
#  ifdef WINDOWS
		BOOL ok = WriteFile(pipes[StdIn][PipeWrite], data.data(), data.size(), &sz, NULL);
		if (!ok) sz = 0;
#  else
		size_t offset = 0;
		while (offset < data.size()) {
			ssize_t wr = ::write(pipes[StdIn][PipeWrite], data.data() + offset, data.size() - offset);
			if (wr < 0) {
				if (errno == EINTR) continue;
				return false;
			}
			offset += wr;
		}
		sz = offset;
#  endif
		return sz == (SizeType)data.size_s();
	}

PRIVATE_DEFINITION_END(PIProcess)


PIProcess::PIProcess(): PIThread() {
	exit_code = -1;
#  ifdef WINDOWS
	PRIVATE->pi.dwProcessId = 0;
#  else
	PRIVATE->pid = 0;
	PRIVATE->forEachPipe([](PipeHandleType & pipe) { pipe = -1; });
#  endif
	exec_start    = false;
	exec_finished = false;
	PRIVATE->initGrab();
	env = PIProcess::currentEnvironment();
}


PIProcess::~PIProcess() {
	PIThread::stopAndWait();
	PRIVATE->closeAllPipes();
}


void PIProcess::exec_() {
	exec_finished = false;
	exec_start    = false;
	PRIVATE->closeAllPipes();
	startOnce();
}


void PIProcess::startProc(bool detached) {
	const PIString & str = args.front();
	if (!detached) execStarted(str);
	if (!PRIVATE->createPipes()) return;
#  ifdef WINDOWS
	STARTUPINFOA si;
	piZeroMemory(si);
	si.cb = sizeof(STARTUPINFOA);
	if (PRIVATE->grab[StdIn]) si.hStdInput = PRIVATE->pipes[StdIn][PipeRead];
	if (PRIVATE->grab[StdOut]) si.hStdOutput = PRIVATE->pipes[StdOut][PipeWrite];
	if (PRIVATE->grab[StdErr]) si.hStdError = PRIVATE->pipes[StdErr][PipeWrite];
	si.dwFlags |= STARTF_USESTDHANDLES;
	const auto cmd = convertWindowsCmd(args);
	if (CreateProcessA(0,                                                      // No module name (use command line)
	                   (LPSTR)cmd.data(),                                      // Command line
	                   0,                                                      // Process handle not inheritable
	                   0,                                                      // Thread handle not inheritable
	                   true,                                                   // Set handle inheritance to FALSE
	                   detached ? DETACHED_PROCESS /*CREATE_NEW_CONSOLE*/ : 0, // Creation flags
	                   0,                                                      // Use environment
	                   wd.isEmpty() ? 0 : wd.data(),                           // Use working directory
	                   &si,                                                    // Pointer to STARTUPINFO structure
	                   &(PRIVATE->pi)))                                        // Pointer to PROCESS_INFORMATION structure
	{
		exec_start = true;
		if (!detached) {
			WaitForSingleObject(PRIVATE->pi.hProcess, INFINITE);
			DWORD code = -1;
			if (GetExitCodeProcess(PRIVATE->pi.hProcess, &code) != 0) exit_code = code;
			exec_finished = true;
		}
		CloseHandle(PRIVATE->pi.hThread);
		CloseHandle(PRIVATE->pi.hProcess);
	} else {
		piCoutObj << "\"CreateProcess\" error: %1"_tr("PIProcess").arg(errorString());
	}
#  else
	auto largs = convertToCharArrays(args);
	auto lenv  = convertToCharArrays(env);
	int pid_   = fork();
	if (pid_ < 0) {
		piCoutObj << "\"fork\" error: " << errorString();
		PRIVATE->closeAllPipes();
		delete[] largs;
		delete[] lenv;
		return;
	}
	if (!detached) PRIVATE->pid = pid_;
	if (pid_ == 0) {
		if (!wd.isEmpty()) {
			if (chdir(wd.data()) != 0) piCoutObj << "Error while set working directory";
		}
		PRIVATE->closePipe(StdIn, PipeWrite);
		PRIVATE->closePipe(StdOut, PipeRead);
		PRIVATE->closePipe(StdErr, PipeRead);

		if (PRIVATE->grab[StdIn]) dup2(PRIVATE->pipes[StdIn][PipeRead], STDIN_FILENO);
		if (PRIVATE->grab[StdOut]) dup2(PRIVATE->pipes[StdOut][PipeWrite], STDOUT_FILENO);
		if (PRIVATE->grab[StdErr]) dup2(PRIVATE->pipes[StdErr][PipeWrite], STDERR_FILENO);

		PRIVATE->closePipe(StdIn, PipeRead);
		PRIVATE->closePipe(StdOut, PipeWrite);
		PRIVATE->closePipe(StdErr, PipeWrite);

		execve(str.data(), largs, lenv);
		// normaly execve can't return, if it returns - error occured
		piCoutObj << "\"execve" << str << args << "\" error :" << errorString();
		exit(127);
	} else {
		PRIVATE->closePipe(StdIn, PipeRead);
		PRIVATE->closePipe(StdOut, PipeWrite);
		PRIVATE->closePipe(StdErr, PipeWrite);

		if (PRIVATE->grab[StdOut]) fcntl(PRIVATE->pipes[StdOut][PipeRead], F_SETFL, O_NONBLOCK);
		if (PRIVATE->grab[StdErr]) fcntl(PRIVATE->pipes[StdErr][PipeRead], F_SETFL, O_NONBLOCK);

		exec_start = true;
		if (!detached) {
			waitpid(pid_, &exit_code, 0);
			if (WIFEXITED(exit_code)) {
				exec_finished = WEXITSTATUS(exit_code) != 127;
			}
			pid_ = 0;
			if (!detached) PRIVATE->pid = pid_;
		}
	}
	delete[] largs;
	delete[] lenv;
#  endif
	if (!detached) execFinished(str, exit_code);
	exec_start = false;
}


void PIProcess::terminate() {
#  ifdef WINDOWS
	if (exec_start) {
		if (!TerminateProcess(PRIVATE->pi.hProcess, 0)) return;
	}
	PRIVATE->pi.dwProcessId = 0;
#  else
	if (exec_start) kill(PRIVATE->pid, SIGKILL);
	PRIVATE->pid = 0;
#  endif
}


void PIProcess::execIndependent(const PIString & program, const PIStringList & args_) {
	PIProcess p;
	p.args << program << args_;
	p.startProc(true);
}


int PIProcess::pID() const {
#  ifdef WINDOWS
	return PRIVATE->pi.dwProcessId;
#  else
	return PRIVATE->pid;
#  endif
}


PIByteArray PIProcess::readOutput() {
	return PRIVATE->readPipe(StdOut);
}


PIByteArray PIProcess::readError() {
	return PRIVATE->readPipe(StdErr);
}


bool PIProcess::writeInput(const PIByteArray & data) {
	if (PRIVATE->grab[StdIn]) return PRIVATE->writePipe(data);
	return false;
}


void PIProcess::closeInput() {
	if (PRIVATE->grab[StdIn]) {
#  ifdef WINDOWS
		PRIVATE->writePipe({0x1A});
#  endif
		PRIVATE->closePipe(StdIn, PipeWrite);
	}
}


void PIProcess::enableWriteStdIn(bool on) {
	PRIVATE->grab[StdIn] = on;
}


void PIProcess::enableReadStdOut(bool on) {
	PRIVATE->grab[StdOut] = on;
}


void PIProcess::enableReadStdErr(bool on) {
	PRIVATE->grab[StdErr] = on;
}


int PIProcess::currentPID() {
#  ifdef WINDOWS
	return GetCurrentProcessId();
#  else
	return getpid();
#  endif
}


PIStringList PIProcess::currentEnvironment() {
	PIStringList l;
	int i = 0;
	while (environ[i] != 0) {
		l << environ[i];
		++i;
	}
	return l;
}


void PIProcess::run() {
	startProc(false);
}


void PIProcess::removeEnvironmentVariable(const PIString & variable) {
	PIString s;
	for (int i = 0; i < env.size_s(); ++i) {
		s = env[i];
		if (s.left(s.find("=")).trimmed() == variable) {
			env.remove(i);
			--i;
		}
	}
}


void PIProcess::setEnvironmentVariable(const PIString & variable, const PIString & value) {
	PIString s, v;
	for (int i = 0; i < env.size_s(); ++i) {
		s = env[i];
		v = s.left(s.find("=")).trimmed();
		if (v == variable) {
			env[i] = v + "=" + value;
			return;
		}
	}
	env << variable + "=" + value;
}


PIString PIProcess::getEnvironmentVariable(const PIString & variable) {
	PIStringList env_ = currentEnvironment();
	PIString s, v;
	for (int i = 0; i < env_.size_s(); ++i) {
		s = env_[i];
		v = s.left(s.find("=")).trimmed();
		if (v == variable) {
			return s.right(s.size() - 1 - s.find("=")).trimmed();
		}
	}
	return PIString();
}

#endif // MICRO_PIP
