/*
 * Shell implementation using flex/bison.
 *
 * Sander van Veen (6167969) / Taddeus Kroes (6054129).
 * <sandervv@gmail.com>      / <taddeuskroes@hotmail.com>
 */

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdarg.h>
#include <math.h>
#include <signal.h>
#include <sys/wait.h>

#include "input.yacc.h"
#include "input.lex.h"
#include "input.l.h"

/*
 * Buffer size of path concatenation.
 */
#define PATH_LENGTH 1024

/*
 * When the shell is attached to a terminal, this value is true. If the shell
 * is attached to a pipe, this value is false.
 */
int is_interactive = 0;

/*
 * Toggle debug messages. Only useful dring the development.
 */
int is_debug_mode = 1;

/*
 * Display a non-fatal error message in stderr.
 */
void shell_error(const char *msg) {
    fprintf(stderr, "error: %s\n", msg);
}

/*
 * Return the length of an unsigned integer.
 */
inline int base10len(unsigned int n) {
    int len = 0;
    /* n < 10^16 */
    if (n >= 100000000) { n /= 100000000; len += 8; }
    /* n < 100000000 = 10^8 */
    if (n >= 10000) { n /= 10000; len += 4; }
    /* n < 10000 */
    if (n >= 100) { n /= 100; len += 2; }
    /* n < 100 */
    if (n >= 10)
        return len + 2;
    else
        return len + 1;
}

/*
 * Display a debug message, when running in debug mode, in stderr.
 */
void shell_debug(const char *msg, ...) {
    if( !is_debug_mode )
        return;

    // Count all '%' chars in message
    int var_count = 0;
    int buf_size = strlen(msg) + 1;
    char *var_types = malloc(buf_size * sizeof(char));
    for( int c = 0; c < buf_size; c++ )
        if( msg[c] == '%' ) {
            assert(c+1 < buf_size);
            var_types[var_count] = msg[c+1];
            var_count++;
        }

    // Calculate message buffer size.
    va_list ap;
    va_start(ap, msg);
    int v;
    for( int i = 0; i < var_count; i++ ) {
        switch( var_types[i] ) {
            case 's': buf_size += strlen(va_arg(ap, char *)); break;
            case 'd':
                v = va_arg(ap, int);
                if( v == 0 )
                    buf_size++;
                else if( v > 0 )
                    buf_size += base10len((unsigned int) v);
                else
                    buf_size += base10len((unsigned int) (v*-1));
            break;
            default: perror("Unsupported variable type used in debug message.");
        }
    }
    va_end(ap);

    // Assemble the debug message.
    va_start(ap, msg);
    char *buf = malloc(buf_size * sizeof(char));
    if( vsnprintf(buf, buf_size, msg, ap) >= buf_size )
        shell_error("shell_debug() failed to assemble message.");
    else
        fprintf(stderr, "debug: %s\n", buf);

    // Clean up used memory.
    free(var_types);
    free(buf);
}

shell_args *cmd_args = NULL;
shell_args *last_arg = NULL;

void shell_push_args(const char *value) {
    assert(cmd_args == NULL && last_arg == NULL);

    last_arg = malloc(sizeof(shell_args));
    last_arg->arg = strdup(value);
    last_arg->next = NULL;

    cmd_args = last_arg;
}

void shell_append_args(const char *value) {
    assert(cmd_args != NULL && last_arg != NULL);

    shell_args *next_arg = malloc(sizeof(shell_args));
    next_arg->arg = strdup(value);
    next_arg->next = NULL;

    last_arg->next = next_arg;
    last_arg = next_arg;
}

shell_args *shell_pop_args() {
    if( !last_arg )
        return NULL;
    assert(cmd_args && last_arg != NULL);
    shell_args *args = cmd_args;
    cmd_args = last_arg = NULL;
    return args;
}

int command_has_pipe = 0;

void shell_pipe() {
    shell_debug("start pipe");
    command_has_pipe = 1;
}

/*
 * Execute the binary and its parsed arguments.
 */
void shell_exec(shell_args *args) {
    if( !args )
        return;
    assert(args && args->arg);
    shell_debug("exec `%s'", args->arg);

    // TODO: handle arguments.
    shell_args *cur = args->next;
    while(cur) {
        shell_debug("argument: `%s'", cur->arg);
        cur = cur->next;
    }

    int pipes[2];
    int has_pipe = 0;

    if( command_has_pipe ) {
        pipe(pipes);
        has_pipe = 1;
    }

    // Disable pipe flag
    command_has_pipe = 0;

    // Fork the process and handle forking error.
    pid_t pid = fork();
    switch( pid ) {
        case -1:
            shell_error("Error when forking");
            exit(EXIT_FAILURE);
        case 0:
            shell_debug("Child process #%d is ready.", getpid());

            if( has_pipe ) {
                if (dup2(pipes[1], 1) != 1)
                    perror("Pipe redirection output error");
                close(pipes[0]);
                close(pipes[1]);
            }

            shell_exec_process(args);
        break;
        default:
            shell_debug("Parent launched child process #%d.", pid);
            if( has_pipe ) {
                if (dup2(pipes[0], 0) != 0)
                    perror("Pipe redirection input error");
                close(pipes[0]);
                close(pipes[1]);
                waitpid(pid, NULL, 0);
            }

            shell_history_append(FN_EXEC, args);
    }
}

/*
 * Helper function for shell_exec(), which executes the process using execv()
 * and handles all occurred errors caused by execv().
 */
void shell_exec_process(shell_args *args) {
    assert(args && args->arg);

    char *bin = malloc((5 + strlen(args->arg) + 1) * sizeof(char));
    bin = strcpy(strcpy(bin, "/bin/")+5, args->arg)-5;

    // Count the arguments.
    int argc = 0;
    for( shell_args *arg = args; arg; arg = arg->next, argc++);

    // Convert argument structure to list of strings. And add a NULL pointer as
    // a marker for the end of the list. The first string should point to the
    // binary being executed.
    assert(argc >= 1);
    char **argv = malloc((argc+1) * sizeof(char *));
    argv[0] = bin;
    shell_args *cur = args + sizeof(shell_args *);
    for( int a = 1; a < argc && cur; cur = args->next, a++)
        argv[a] = strdup(cur->arg);
    argv[argc] = NULL;

    // Function execvp() searches in PATH-environment, which is more flexible
    // (user can set its own preferences), but the assignment states execv()
    // should be used, because only binaries in /bin should be loaded.
    shell_debug("execv(%s, [...]) called.\n", bin);
    execv(bin, argv);

    // If the execv() call returns, an error occurred and errno contains the
    // error number corresponding with the error occurred.
    switch( errno ) {
        case E2BIG:
            shell_error("The total number of bytes in the environment (envp)"
                    " and argument list (argv) is too large.");
        break;
        case EACCES:
            shell_error("Search permission is denied on a component of the"
                    " path prefix of filename or the name of a script "
                    " interpreter. The file or a script interpreter is not a"
                    " regular file. Execute permission is denied for the file"
                    " or a script or ELF interpreter. The file system is"
                    " mounted noexec.");
        break;
        case EFAULT:
            shell_error("Filename points outside accessible address space.");
        break;
        case EINVAL:
            shell_error("An ELF executable had more than one PT_INTERP segment"
                    " (i.e., tried to name more than one interpreter).");
        break;
        case EIO:
            shell_error("An I/O error occurred.");
        break;
        case EISDIR:
            shell_error("An ELF interpreter was a directory.");
        break;
        case ELIBBAD:
            shell_error("An ELF interpreter was not in a recognized format.");
        break;
        case ELOOP:
            shell_error("Too many symbolic links were encountered in resolving"
                    " filename or the name of a script or ELF interpreter.");
        break;
        case EMFILE:
            shell_error("The process has the maximum number of files open.");
        break;
        case ENAMETOOLONG:
            shell_error("Filename is too long.");
        break;
        case ENFILE:
            shell_error("The system limit on the total number of open files"
                    " has been reached.");
        break;
        case ENOENT:
            shell_error("The file filename or a script or ELF interpreter does"
                    " not exist, or a shared library needed for file or"
                    " interpreter cannot be found.");
        break;
        case ENOEXEC:
            shell_error("An executable is not in a recognized format, is for"
                    " the wrong architecture, or has some other format error"
                    " that means it cannot be executed.");
        break;
        case ENOMEM:
            shell_error("Insufficient kernel memory was available.");
        break;
        case ENOTDIR:
            shell_error("A component of the path prefix of filename or a"
                    " script or ELF interpreter is not a directory.");
        break;
        case EPERM:
            shell_error("The file system is mounted nosuid, the user is"
                    " not the superuser, and the file has the set-user-ID or"
                    " set-group-ID bit set. The process is being traced, the"
                    " user is not the superuser and the file has the "
                    " set-user-ID or set-group-ID bit set.");
        break;
        case ETXTBSY:
            shell_error("Executable was open for writing by one or more"
                    " processes.");
        break;
    }
}

/*
 * Gracefully exit the shell.
 */
void shell_exit() {
    shell_debug("Exiting shell");

    // Append to command history.
    shell_history_append(FN_EXIT, NULL);

    exit(EXIT_SUCCESS);
}

void shell_help() {
    // TODO: implement help text.
    puts("Builtin commands / macros:");
    puts(" cd PATH - Change current working directory to PATH.");
    puts(" exit    - Exit this shell (also CTRL + D).");
    puts(" help    - This help text.");
    puts(" !!      - Repeat previous shell command.");
    puts(" !N      - Repeat Nth previous shell command.");

    // Append to command history.
    shell_history_append(FN_HELP, NULL);
}

/*
 * Change the current working directory of the shell. This function handles
 * absolute and relative paths and will return 0 when the directory is
 * successfully changed, or otherwise 1 in case of an error (e.g. path does not
 * exist).
 */
void shell_change_dir(const char *path) {
    shell_debug("change directory: %s", path);
    char *cur = malloc((PATH_LENGTH+1) * sizeof(char));
    shell_debug("before: %s", getcwd(cur, PATH_LENGTH));

    // Depending on the file system, other errors can be returned.
    switch( chdir(path) ) {
        case 0:
            // No error, always a good sign :-).
        break;
        case EACCES:
            shell_error("Search permissions denied for path.");
        break;
        case EFAULT:
            shell_error("Path points outside your accessible address space.");
        break;
        case EIO:
            shell_error("An I/O error occurred.");
        break;
        case ELOOP:
            shell_error("Too many symbolic links while resolving path.");
        break;
        case ENAMETOOLONG:
            shell_error("Path is too long.");
        break;
        case ENOENT:
            shell_error("The file does not exist.");
        break;
        case ENOMEM:
            shell_error("Insufficient kernel memory was available.");
        break;
        case ENOTDIR:
            shell_error("Path is not a directory.");
        break;
        default:
            shell_error("Unknown error ocurred while changing directory.");
        break;
    }

    shell_debug("after:  %s", getcwd(cur, PATH_LENGTH));

    // Append to command history.
    shell_push_args(path);
    shell_history_append(FN_EXEC, shell_pop_args());
}

/*
 * Each new line will start a new shell entry ("$ "), which is an indicator to
 * the user that he is able to enter another command. This function will not do
 * anything when there is no terminal attached to the shell (but, for example, a
 * pipe to prevent the printing of redundant characters).
 */
void shell_entry() {
    if( !is_interactive )
        return;
    printf("$ ");
    fflush(stdout);
}

/*
 * Doubly linked list containing the command history of this shell.
 */
shell_history_log *shell_history = NULL;
int shell_history_count = 0;

shell_cmd *shell_history_get(int prev) {
    assert(prev >= 1);

    if( prev >= shell_history_count )
        return NULL;

    shell_history_log *cur = shell_history;
    while( cur && cur->prev && --prev ) {
        assert(cur);
        assert(cur->cmd);
        assert(cur->cmd->args);
        shell_debug("skipping [%d]: %s\n", prev, cur->cmd->args->arg);
        cur = cur->prev;
    }

    if( cur )
        shell_debug("found [%d]: %s\n", prev, cur->cmd->args->arg);

    return cur ? cur->cmd : NULL ;
}

void shell_history_add(shell_cmd *cmd) {
    assert(cmd);

    if( !shell_history ) {
        shell_history = malloc(sizeof(shell_history_log));
        shell_history->cmd = cmd;
        shell_history->prev = shell_history->next = NULL;
        shell_history_count = 1;
    }
    else {
        shell_history_log *history = malloc(sizeof(shell_history_log));
        history->cmd = cmd;
        history->next = NULL;
        history->prev = shell_history;
        shell_history->next = history;
        shell_history = history;
        shell_history_count++;
    }
}

void shell_history_clear() {
    // TODO: free shell command history.
}

void shell_history_append(shell_func type, shell_args *args) {
    shell_cmd *cmd = malloc(sizeof(shell_cmd));
    cmd->func = type;
    cmd->args = args;
    shell_history_add(cmd);
}

/*
 * Implementation of the repeat builtin shell command.
 */
void shell_repeat(int prev) {
    shell_debug("repeat %d", prev);

    assert(prev > 0);

    // TODO: implement repeat builtin.
    shell_cmd *cmd;
    if( !(cmd = shell_history_get(prev)) ) {
        shell_error("Cannot repeat command before recorded history.");
        return;
    }

    // TODO: execute `cmd'

    shell_history_add(cmd);
}

/*
 * Process number of the parent shell.
 */
pid_t parent_pid;

/*
 * Signal handler for SIGINT, SIGTERM and SIGCHLD.
 */
void signal_handler(int signum) {
    char *sig = "TERM";
    pid_t pid;

    // TODO: implement signal handling

    switch( signum ) {
        case SIGINT:
            sig = "INT";
        case SIGTERM:
            if( (pid = getpid()) == parent_pid )
                printf("Parent process received SIG%s and will exit.\n", sig);
            else
                printf("Process #%d received SIG%s and will exit.\n", pid, sig);
            exit(1);
        case SIGCHLD:
            puts("Parent process received SIGCHLD.");
            pid = wait(NULL);
            printf("Child with id %d exited.\n", pid);
        break;
        default:
            puts("This handler does not understand this type of signal.");
    }
}

/*
 * Main function, initiates the program.
 */
int main(void) {
    struct sigaction action = {.sa_handler = signal_handler};

    parent_pid = getpid();

    // Bind signal handlers
    if( sigaction(SIGINT, &action, NULL)
        || sigaction(SIGTERM, &action, NULL)
        || sigaction(SIGCHLD, &action, NULL) )
    {
        perror("An error occured while binding the signal handlers.\n");
        exit(1);
    }

    // Detect if terminal is attached.
    struct stat stats;
    fstat(0, &stats);

    // Looks like a tty, so we're in interactive mode.
    if (S_ISCHR(stats.st_mode))
        is_interactive = 1;
    else if (S_ISFIFO(stats.st_mode))
        // Looks like a pipe, so we're in non-interactive mode.
        is_interactive = 0;
    else
        is_interactive = 1;

    // Create the first shell entry.
    shell_entry();

    // Disable yacc debug mode.
    yyset_debug(0);

    // Start the shell parser.
    if( yyparse() ) {
        fprintf(stderr, "Error: Parsing failed\n");
        return EXIT_FAILURE;
    }

    // EOF reached: clean up the mess.
    shell_history_clear();
    yylex_destroy();

    if( yyin )
        fclose(yyin);

    return EXIT_SUCCESS;
}
