#include "cmd_exec.h"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#include "builtins.h"
#include "sish.h"
#include "utils.h"

/* Taught in CS 392 to label for clairity's sake */
#define READ_FD 0
#define WRITE_FD 1

int
parse_commandline(char** token_args, CMD_Pipeline* p)
{
	if (token_args == NULL || token_args[0] == NULL)
	{
		return -1;
	}

	int toffset = 0;
	while (token_args[toffset] != NULL)
	{
		toffset++;
	}

	/* consume & if present */
	if (toffset > 0 && strcmp(token_args[toffset - 1], "&") == 0)
	{
		p->run_in_background = true;
		token_args[toffset - 1] = NULL;
		toffset--;

		/* if the user only provided a & well uh*/
		if (toffset == 0)
		{
			/* message stolen from linux shell */
			fprintf(stderr, "%s: syntax error near unexpected token '&'\n",
			        GET_PROGNAME_ERR);
			return -1;
		}
	}

	/* scan redirections */
	int input_file_alr_given = 0;
	int output_file_pos = -1;
	int output_file_mode = M_TRUNC;

	for (int i = 0; i < toffset; i++)
	{

		if (strcmp(token_args[i], "<") == 0)
		{
			input_file_alr_given++;
			if (input_file_alr_given > 1)
			{
				fprintf(stderr,
				        "%s: multiple input redirections not supported\n",
				        GET_PROGNAME_ERR);
				return -1;
			}

			if (token_args[i + 1] == NULL)
			{
				/* no input file specified - also mostly what the default linux
				 * shell says */
				fprintf(stderr,
				        "%s: syntax error near unexpected token 'newline' "
				        "(input file not found)\n",
				        GET_PROGNAME_ERR);
				return -1;
			}
			p->infile = token_args[i + 1];
		}

		if (strcmp(token_args[i], ">") == 0 || strcmp(token_args[i], ">>") == 0)
		{
			if (token_args[i + 1] == NULL)
			{
				fprintf(stderr,
				        "%s: syntax error near unexpected token 'newline' "
				        "(output file not found)\n",
				        GET_PROGNAME_ERR);
				return -1;
			}

			output_file_pos = i;
			if (token_args[i][1] == '>')
			{
				output_file_mode = M_APPEND;
			}
			else
			{
				output_file_mode = M_TRUNC;
			}
		}
	}

	if (output_file_pos != -1)
	{
		p->outfile = token_args[output_file_pos + 1];
		p->append = output_file_mode;
	}

	int cmd = 0;
	p->num_commands = 1;

	for (int i = 0; i < toffset; i++)
	{
		if (strcmp(token_args[i], "<") == 0 ||
		    strcmp(token_args[i], ">") == 0 || strcmp(token_args[i], ">>") == 0)
		{
			i++;
			continue;
		}

		if (strcmp(token_args[i], "|") == 0)
		{
			if (p->commands[cmd].cmd_and_args[0] == '\0')
			{
				/* also taken from linux shell */
				fprintf(stderr, "%s: syntax error near unexpected token '|'\n",
				        GET_PROGNAME_ERR);
				return -1;
			}

			cmd++;
			if (cmd >= MAX_COMMANDS)
			{
				fprintf(stderr, "%s: too many commands in pipeline (max %d)\n",
				        GET_PROGNAME_ERR, MAX_COMMANDS);
				return -1;
			}

			p->num_commands++;
			continue;
		}

		if (p->commands[cmd].cmd_and_args[0] != '\0')
		{
			strncat(p->commands[cmd].cmd_and_args, " ",
			        sizeof(p->commands[cmd].cmd_and_args) - 1);
		}

		strncat(p->commands[cmd].cmd_and_args, token_args[i],
		        sizeof(p->commands[cmd].cmd_and_args) - 1);
	}

	if (p->commands[cmd].cmd_and_args[0] == '\0')
	{
		return -1;
	}

	/* apply redirection to commands */
	if (p->infile != NULL)
	{
		p->commands[0].redir_stdin = p->infile;
	}

	if (p->outfile != NULL)
	{
		p->commands[p->num_commands - 1].redir_stdout = p->outfile;
		p->commands[p->num_commands - 1].redir_out_mode =
			p->append ? M_APPEND : M_TRUNC;
	}
	return 0;
}

void
get_cmd_name_from_cmd_and_args(const char* cmd_and_args, char* cmd_name,
                               int cmd_name_bufsiz)
{
	int len = 0;
	while (*(cmd_and_args + len) != '\0' &&
	       !isspace((unsigned char)(*(cmd_and_args + len))))
	{
		len++;

		if (len >= cmd_name_bufsiz)
		{
			len = cmd_name_bufsiz - 1;
			break;
		}
	}

	memcpy(cmd_name, cmd_and_args, len);
	cmd_name[len] = '\0';

	return;
}

bool
should_run_builtin_in_parent(const CMD_Pipeline* p)
{
	if (p->num_commands != 1)
	{
		return false;
	}

	const Command* c = &(p->commands[0]);

	if (c->redir_stdin != NULL || c->redir_stdout != NULL)
	{
		return false;
	}

	char cmd_name[BUFSIZ] = {0};
	get_cmd_name_from_cmd_and_args(c->cmd_and_args, cmd_name, sizeof(cmd_name));

	return is_builtin(cmd_name);
}

/* for sole use with execv */
void
split_cmd_into_argv(const char* cmd, char** argv)
{
	char* tmp = strdup(cmd);
	int i = 0;
	char* tok = strtok(tmp, " ");
	while (tok != NULL && i < MAX_ARGS - 1)
	{
		argv[i] = tok;
		i++;
		tok = strtok(NULL, " ");
	}
	argv[i] = NULL;
	/* tmp intentionally not freed as we would need to free after exec, there's
	 * a comment after the exec call with more info*/
}

void
try_run_builtin(const char* cmd_name, const char* cmd_and_args)
{
	char* args_buf = strdup(cmd_and_args);
	if (args_buf == NULL)
	{
		fprintf(stderr, "%s: strdup failed: %s\n", GET_PROGNAME_ERR,
		        strerror(errno));
		return;
	}

	char* token_args[MAX_ARGS] = {0};
	int argc = split_builtin_args(args_buf, token_args);

	if (strcmp(cmd_name, "cd") == 0)
	{
		last_fg_exit_status = sish_cd(token_args, argc);
		goto ret;
	}

	if (strcmp(cmd_name, "echo") == 0)
	{
		last_fg_exit_status = sish_echo(token_args, argc);
		goto ret;
	}

	if (strcmp(cmd_name, "exit") == 0)
	{
		free(args_buf);
		sish_exit(EXIT_SUCCESS);
		/* unreachable */
	}

ret:
	free(args_buf);
	return;
}

void
trace(CMD_Pipeline* p)
{
	for (int i = 0; i < p->num_commands; i++)
	{
		fprintf(stderr, "+ %s\n", p->commands[i].cmd_and_args);
	}

	return;
}

void
run_commands(char** token_args)
{
	CMD_Pipeline p = {0};

	for (int i = 0; i < MAX_COMMANDS; i++)
	{
		memset(&p.commands[i], 0, sizeof(p.commands[i]));
	}

	int retval = parse_commandline(token_args, &p);
	if (flag_tracing_enabled)
	{
		trace(&p);
	}

	if (retval != 0)
	{
		/* error propegation handled in parse_commandline */
		return;
	}

	int num_commands = p.num_commands;
	if (num_commands <= 0)
	{
		fprintf(stderr,
		        "%s: command pipeline could not run: Invalid number of "
		        "commands to process: %d\n",
		        GET_PROGNAME_ERR, num_commands);
		return;
	}

	/* needed for io redirection to happen (nicely) with builtins; see below */
	if (should_run_builtin_in_parent(&p))
	{
		char cmd_name[BUFSIZ] = {0};
		get_cmd_name_from_cmd_and_args(p.commands[0].cmd_and_args, cmd_name,
		                               sizeof(cmd_name));

		try_run_builtin(cmd_name, p.commands[0].cmd_and_args);
		return;
	}

	/* a | b | c; n - 1 pipes needed at most */
	int pipes[num_commands - 1][2];

	for (int i = 0; i < num_commands - 1; i++)
	{
		if (pipe(pipes[i]) < 0)
		{
			fprintf(stderr, "%s: pipe failed: %s\n", GET_PROGNAME_ERR,
			        strerror(errno));
			exit(EXIT_FAILURE);
		}
	}

	for (int i = 0; i < num_commands; i++)
	{

		pid_t pid = fork();
		if (pid < 0)
		{
			fprintf(stderr, "%s: fork failed: %s\n", GET_PROGNAME_ERR,
			        strerror(errno));
			exit(EXIT_FAILURE);
		}

		/* if parent and background command set last pid to the most recent
		 * background task. Unsure whether this should only apply to the first
		 * command, very last, or what -- but for now I am interpreting it as
		 * literally "the most recent" */
		if (/*i == 0 && */ pid > 0 && p.run_in_background)
		{
			last_pid_bg = pid;
			printf("[%d]\n", pid);
		}

		/* CHILD */
		if (pid == 0)
		{
			if (i > 0)
			{
				if (dup2(pipes[i - 1][READ_FD], STDIN_FILENO) == -1)
				{
					fprintf(stderr, "%s: dup2 failed: %s\n", GET_PROGNAME_ERR,
					        strerror(errno));
					exit(EXIT_FAILURE);
				}
			}

			if (i < num_commands - 1)
			{
				if (dup2(pipes[i][WRITE_FD], STDOUT_FILENO) == -1)
				{

					fprintf(stderr, "%s: dup2 failed: %s\n", GET_PROGNAME_ERR,
					        strerror(errno));
					exit(EXIT_FAILURE);
				}
			}

			/* close now-useless pipes although it's kind of unnecessary */
			for (int j = 0; j < num_commands - 1; j++)
			{
				(void)close(pipes[j][READ_FD]);
				(void)close(pipes[j][WRITE_FD]);
			}

			/* input redirection */
			if (p.commands[i].redir_stdin)
			{
				int fd = open(p.commands[i].redir_stdin, O_RDONLY);
				if (fd < 0)
				{
					fprintf(stderr, "%s: unable to open %s: %s\n",
					        GET_PROGNAME_ERR, p.commands[i].redir_stdin,
					        strerror(errno));
					exit(EXIT_FAILURE);
				}
				(void)dup2(fd, STDIN_FILENO);
				(void)close(fd);
			}

			/* output redirection */
			if (p.commands[i].redir_stdout)
			{
				int flags = O_WRONLY | O_CREAT;
				if (p.commands[i].redir_out_mode == M_APPEND)
				{
					flags |= O_APPEND;
				}
				else
				{
					flags |= O_TRUNC;
				}
				int fd = open(p.commands[i].redir_stdout, flags, 0644);
				if (fd < 0)
				{
					fprintf(stderr, "%s: unable to open %s: %s\n",
					        GET_PROGNAME_ERR, p.commands[i].redir_stdout,
					        strerror(errno));
					exit(EXIT_FAILURE);
				}
				(void)dup2(fd, STDOUT_FILENO);
				(void)close(fd);
			}

			/* run builtin or external */
			char cmd_name[BUFSIZ] = {0};
			get_cmd_name_from_cmd_and_args(p.commands[i].cmd_and_args, cmd_name,
			                               sizeof(cmd_name));

			if (is_builtin(cmd_name))
			{
				/* builtin modifies last_fg_exit_status */
				try_run_builtin(cmd_name, p.commands[i].cmd_and_args);
				exit(last_fg_exit_status);
			}

			/* external command */
			char* argv[MAX_ARGS] = {0};

			split_cmd_into_argv(p.commands[i].cmd_and_args, argv);

			execvp(argv[0], argv);
			/* normally unreachable */
			/* at this point the malloced memory from split_cmd_into_argv is
			 * "leaked" but we exit immediately after anyway */

			fprintf(stderr, "%s: \"%s\": command not found\n", GET_PROGNAME_ERR,
			        argv[0]);

			exit(127); /* per the spec */
		}
	}

	/* clean up in sish/parent */
	for (int i = 0; i < num_commands - 1; i++)
	{
		(void)close(pipes[i][READ_FD]);
		(void)close(pipes[i][WRITE_FD]);
	}

	if (!p.run_in_background)
	{
		for (int i = 0; i < num_commands; i++)
		{
			/* if we needed to keep track of return values/status of multiple
			 * sub processes and such (possibly simultaneously), an array of the
			 * result of fork + waitpid would work better but that's not
			 * required here, so wait waiting on as many child processes as were
			 * spawned works fine, overwriting the last exit status aside */
			(void)wait(&last_fg_exit_status);
		}
	}

	return;
}