#include "utils.h"

#include <ctype.h>
#include <errno.h>
#include <libgen.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "sish.h"

/* for PATH_MAX */
#ifdef __NetBSD__
#include <sys/syslimits.h>
#endif

#ifdef __linux__
#include <linux/limits.h>
#endif

#if defined(__sun) && defined(__illumos__)
#include <limits.h>
#endif

static void (*default_sigint_handler)(int) = NULL;
static void (*default_sigquit_handler)(int) = NULL;
static void (*default_sigtstop_handler)(int) = NULL;

/* 20 by getting the max number of digits of an 64 bit integer, i.e.,
 * ceil(log_10(2^64)), even though pid_t is usually 4 bytes and exit
 * statuses, 1. */
static char last_pid_bg_STRING[20] = {0};
static char last_fg_exit_status_STRING[20] = {0};
static char sish_pid_STRING[20] = {0};

/* there is likely a much better / safer way to do this by performing lookups in
 * /proc or something but as far as I know this is heavily operating system
 * specific and below I have done my best to make an intuitive mostly
 * cross-platform implementation */
void
set_shell_env(char* arg)
{
	char sish_path[PATH_MAX] = {0};

	char cwd[PATH_MAX] = {0};
	if (getcwd(cwd, sizeof(cwd)) == NULL)
	{
		fprintf(stderr, "%s: set_shell_env: getcwd: %s\n", GET_PROGNAME_ERR,
		        strerror(errno));
		exit(EXIT_FAILURE);
	}

	char progname[BUFSIZ] = {0};
#ifdef __linux__
	(void)strncpy(progname, basename(arg), sizeof(progname));
#else
	/* very surprised the compiler didn't catch this, leaving me to explicitly
	 * cast to void -- it makes sense as to why knowing the stages of
	 * compilation, but still */
	(void)arg;
	(void)strncpy(progname, getprogname(), sizeof(progname) - 1);
#endif

	int n = snprintf(sish_path, sizeof(sish_path), "%s/%s", cwd, progname);
	if (n < 0 || (size_t)n >= sizeof(sish_path))
	{
		fprintf(stderr, "%s: set_shell_env: path too long\n", GET_PROGNAME_ERR);
		exit(EXIT_FAILURE);
	}

	if (setenv("SHELL", sish_path, true) == -1)
	{
		fprintf(stderr, "%s: set_shell_env: setenv: %s\n", GET_PROGNAME_ERR,
		        strerror(errno));
		exit(EXIT_FAILURE);
	}
}

void
try_expand_env_vars(char** argv)
{
	int i = 0;

	/* args is (should be) null terminated */
	while (argv[i] != NULL)
	{
		char* arg = argv[i];
		int len = strlen(arg);

		if (arg[0] != '$' || len < 2 || arg[1] == '\0')
		{
			i++;
			continue;
		}

		char* env_val = NULL;

		/* in instances like $$$*, the entire string will turn into say, the
		 * shell's PID, whereas in sh(1), it would turn into <pid>$*, where *
		 * represents >= 0 more $ symbols */
		switch (arg[1])
		{
		case '!':
			snprintf(last_pid_bg_STRING, sizeof(last_pid_bg_STRING), "%d",
			         last_pid_bg);
			env_val = last_pid_bg_STRING;
			break;
		case '?':
			snprintf(last_fg_exit_status_STRING,
			         sizeof(last_fg_exit_status_STRING), "%d",
			         WEXITSTATUS(last_fg_exit_status));
			env_val = last_fg_exit_status_STRING;
			break;
		case '$':
			snprintf(sish_pid_STRING, sizeof(sish_pid_STRING), "%d", getpid());
			env_val = sish_pid_STRING;
			break;

		default:
			/* skip the leading $*/
			env_val = getenv(argv[i] + 1);
			break;
		}

		void* old = argv[i];
		char* tmp = NULL;

		if (env_val == NULL)
		{
			tmp = malloc(1);
			if (tmp == NULL)
			{
				fprintf(stderr, "%s: try_expand_env_vars: malloc failed: %s\n",
				        GET_PROGNAME_ERR, strerror(errno));
				return;
			}
			tmp[0] = '\0';
		}
		else
		{
			tmp = malloc(strlen(env_val) + 1);
			if (tmp == NULL)
			{
				fprintf(stderr, "%s: try_expand_env_vars: malloc failed: %s\n",
				        GET_PROGNAME_ERR, strerror(errno));
				return;
			}
			/* surely strcpy is acceptable here, surely */
			strcpy(tmp, env_val);
		}

		/* free only after success */
		free(old);
		argv[i] = tmp;

		i++;
	}

	return;
}

/* a monstrosity of an attempt at adapting strtok/a previous call made to it to
 * ignore whitespaces while parsing user input */
int
tokenize(char* input, char** token_out)
{
	int token_args_count = 0;
	char* p = input;

	while (*p != '\0')
	{
		while (*p != '\0' && isspace((unsigned char)*p))
		{
			p++;
		}

		if (IS_SHELL_SPECIAL_C(*p))
		{
			int len = 1;
			if ((*p == '>') && *(p + 1) == '>')
			{
				len = 2;
			}

			char* tok = malloc(len + 1);
			if (tok == NULL)
			{
				fprintf(stderr, "%s: tokenize: malloc: %s\n", GET_PROGNAME_ERR,
				        strerror(errno));
				goto try_free_tokens;
			}

			memcpy(tok, p, len);
			tok[len] = '\0';
			token_out[token_args_count] = tok;
			token_args_count++;

			p += len;
			continue;
		}

		char* start = p;
		while (*p != '\0' && !IS_SHELL_SPECIAL_C(*p))
		{
			if (isspace((unsigned char)*p))
			{
				if (p > start)
				{
					int len = (int)(p - start);
					char* tok = malloc(len + 1);
					if (tok == NULL)
					{
						fprintf(stderr, "%s: tokenize: malloc: %s\n",
						        GET_PROGNAME_ERR, strerror(errno));
						goto try_free_tokens;
					}

					memcpy(tok, start, len);
					tok[len] = '\0';
					token_out[token_args_count] = tok;
					token_args_count++;
				}

				while (*p != '\0' && isspace((unsigned char)*p))
				{
					p++;
				}
				start = p;
			}
			else
			{
				p++;
			}
		}

		if (p > start)
		{
			int len = (int)(p - start);
			char* tok = malloc(len + 1);
			if (tok == NULL)
			{
				fprintf(stderr, "%s: tokenize: malloc: %s\n", GET_PROGNAME_ERR,
				        strerror(errno));
				goto try_free_tokens;
			}

			memcpy(tok, start, len);
			tok[len] = '\0';
			token_out[token_args_count] = tok;
			token_args_count++;
		}
	}

	token_out[token_args_count] = NULL;
	return token_args_count;

/* malloc may fail after successfully allocated any number of previous tokens */
try_free_tokens:
	for (int i = 0; i < token_args_count; i++)
	{
		free(token_out[i]);
	}
	return -1;
}

static void
signal_handler(int sig)
{
	(void)sig;
	/* taught in CS 392 for that class' minishell project */
	interrupted = true;
	/* async signal safe! */
	write(STDOUT_FILENO, "\n", 1);
}

void
ignore_signals()
{
	const char* err_msg = NULL;

	/* apparently using the library signal(3)
	 * defaults to IO and such not being interuptted / restarting (like fgets),
	 * whereas using sigaction apparently does default to interuptting. Learned
	 * this while replacing my old CS 392 code for the shell with the calls as
	 * mentioned, hence bringing back the old code from there as it just so
	 * happens to work! */
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = signal_handler;
	sigemptyset(&sa.sa_mask);

	/* disrupt IO!!! */
	sa.sa_flags = 0;

	if (sigaction(SIGINT, &sa, NULL) == -1)
	{
		err_msg = "SIGINT";
		goto sig_fail;
	}

	if (sigaction(SIGQUIT, &sa, NULL) == -1)
	{
		err_msg = "SIGQUIT";
		goto sig_fail;
	}

	if (sigaction(SIGTSTP, &sa, NULL) == -1)
	{
		err_msg = "SIGTSTP";
		goto sig_fail;
	}

	/* success */
	return;

sig_fail:
	(void)fprintf(stderr, "%s: Unable to ignore %s. Cannot continue.\n",
	              GET_PROGNAME_ERR, err_msg);
	exit(EXIT_FAILURE);
	/* not reached */
}

void
restore_default_signals()
{
	const char* err_msg = NULL;

	if (signal(SIGINT, default_sigint_handler) == SIG_ERR)
	{
		err_msg = "SIGINT";
		goto sig_restore_fail;
	}

	if (signal(SIGQUIT, default_sigquit_handler) == SIG_ERR)
	{
		err_msg = "SIGQUIT";
		goto sig_restore_fail;
	}

	if (signal(SIGTSTP, default_sigtstop_handler) == SIG_ERR)
	{
		err_msg = "SIGTSTP";
		goto sig_restore_fail;
	}

	/* success */
	return;

sig_restore_fail:
	(void)fprintf(stderr,
	              "%s: Unable to restore %s default handler. Cannot "
	              "continue.\n",
	              GET_PROGNAME_ERR, err_msg);
	exit(EXIT_FAILURE);
	/* not reached */
}