#include "process.h"

#include <fcntl.h>
#include <sys/wait.h>
#include <signal.h>

struct Process {
	pid_t pid;
	int stdout_pipe;
	// only applicable if separate_stderr was specified.
	int stderr_pipe;
	int stdin_pipe;
	char error[64];
};

int process_get_id(void) {
	return getpid();
}

static void set_nonblocking(int fd) {
	fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

bool process_run_ex(Process *proc, const char *command, const ProcessSettings *settings) {
	memset(proc, 0, sizeof *proc);

	int stdin_pipe[2] = {0}, stdout_pipe[2] = {0}, stderr_pipe[2] = {0};
	if (pipe(stdin_pipe) != 0) {
		strbuf_printf(proc->error, "%s", strerror(errno));
		return false; 
	}
	if (pipe(stdout_pipe) != 0) {
		strbuf_printf(proc->error, "%s", strerror(errno));
		close(stdin_pipe[0]);
		close(stdin_pipe[1]);
		return false;
	}
	if (settings->separate_stderr) {
		if (pipe(stderr_pipe) != 0) {
			strbuf_printf(proc->error, "%s", strerror(errno));
			close(stdin_pipe[0]);
			close(stdin_pipe[1]);
			close(stdout_pipe[0]);
			close(stdout_pipe[1]);
			return false;
		}
	}
	
	bool success = false;
	pid_t pid = fork();
	if (pid == 0) {
		// child process
		// put child in its own group. it will be in this group with all of its descendents,
		// so by killing everything in the group, we kill all the descendents of this process.
		// if we didn't do this, we would just be killing the sh process in process_kill.
		setpgid(0, 0);
		// pipe stuff
		dup2(stdout_pipe[1], STDOUT_FILENO);
		if (stderr_pipe[1])
			dup2(stderr_pipe[1], STDERR_FILENO);
		else
			dup2(stdout_pipe[1], STDERR_FILENO);
		dup2(stdin_pipe[0],  STDIN_FILENO);
		// don't need these file descriptors anymore
		close(stdin_pipe[0]);
		close(stdin_pipe[1]);
		close(stdout_pipe[0]);
		close(stdout_pipe[1]);
		if (stderr_pipe[0]) {
			close(stderr_pipe[0]);
			close(stderr_pipe[1]);
		}
		
		char *program = "/bin/sh";
		char *argv[] = {program, "-c", (char *)command, NULL};
		if (execv(program, argv) == -1) {
			dprintf(STDERR_FILENO, "%s: %s\n", program, strerror(errno));
			exit(127);
		}
	} else if (pid > 0) {
		// parent process
		
		// we're reading from (the child's) stdout/stderr and writing to stdin,
		// so we don't need the write end of the stdout pipe or the
		// read end of the stdin pipe.
		close(stdout_pipe[1]);
		if (stderr_pipe[1])	
			close(stderr_pipe[1]);
		close(stdin_pipe[0]);
		// set pipes to non-blocking
		if (!settings->stdout_blocking)
			set_nonblocking(stdout_pipe[0]);
		if (stderr_pipe[0] && !settings->stderr_blocking)
			set_nonblocking(stderr_pipe[0]);
		if (!settings->stdin_blocking)
			set_nonblocking(stdin_pipe[1]);
		proc->pid = pid;
		proc->stdout_pipe = stdout_pipe[0];
		if (stderr_pipe[0])
			proc->stderr_pipe = stderr_pipe[0];
		proc->stdin_pipe = stdin_pipe[1];
		success = true;
	}
	return success;
}

bool process_run(Process *proc, char const *command) {
	const ProcessSettings settings = {0};
	return process_run_ex(proc, command, &settings);
}


char const *process_geterr(Process *p) {
	return *p->error ? p->error : NULL;
}

long long process_write(Process *proc, const char *data, size_t size) {
	assert(proc->stdin_pipe); // check that process hasn't been killed
	ssize_t bytes_written = write(proc->stdin_pipe, data, size);
	if (bytes_written >= 0) {
		return (long long)bytes_written;
	} else if (errno == EAGAIN) {
		return 0;
	} else {
		strbuf_printf(proc->error, "%s", strerror(errno));
		return -2;
	}
}

static long long process_read_fd(Process *proc, int fd, char *data, size_t size) {
	assert(fd);
	ssize_t bytes_read = read(fd, data, size);
	if (bytes_read >= 0) {
		return (long long)bytes_read;
	} else if (errno == EAGAIN) {
		return -1;
	} else {
		strbuf_printf(proc->error, "%s", strerror(errno));
		return -2;
	}
}

long long process_read(Process *proc, char *data, size_t size) {
	return process_read_fd(proc, proc->stdout_pipe, data, size);
}

long long process_read_stderr(Process *proc, char *data, size_t size) {
	return process_read_fd(proc, proc->stderr_pipe, data, size);
}

static void process_close_pipes(Process *proc) {
	close(proc->stdin_pipe);
	close(proc->stdout_pipe);
	close(proc->stderr_pipe);
	proc->stdin_pipe = 0;
	proc->stdout_pipe = 0;
	proc->stderr_pipe = 0;
}

void process_kill(Process *proc) {
	kill(-proc->pid, SIGKILL); // kill everything in process group
	// get rid of zombie process
	waitpid(proc->pid, NULL, 0);
	proc->pid = 0;
	process_close_pipes(proc);
}

int process_check_status(Process *proc, char *message, size_t message_size) {
	int wait_status = 0;
	int ret = waitpid(proc->pid, &wait_status, WNOHANG);
	if (ret == 0) {
		// process still running
		return 0;
	} else if (ret > 0) {
		if (WIFEXITED(wait_status)) {
			process_close_pipes(proc);
			int code = WEXITSTATUS(wait_status);
			if (code == 0) {
				str_printf(message, message_size, "exited successfully");
				return +1;
			} else {
				str_printf(message, message_size, "exited with code %d", code);
				return -1;
			}
		} else if (WIFSIGNALED(wait_status)) {
			process_close_pipes(proc);
			str_printf(message, message_size, "terminated by signal %d", WTERMSIG(wait_status));
			return -1;
		}
		return 0;
	} else {
		// this process is gone or something?
		process_close_pipes(proc);
		str_printf(message, message_size, "process ended unexpectedly");
		return -1;
	}
}