#include <assert.h>
#include <errno.h>
#include <luna.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static int status = 0;

typedef struct
{
    char* buffer;
    size_t size;
    size_t capacity;
} command;

void show_prompt()
{
    if (status) { printf("%d [%ld]> ", WEXITSTATUS(status), getpid()); }
    else
        printf("[%ld]> ", getpid());
}

int command_matches(command* cmd, const char* string)
{
    if (cmd->size <= strlen(string)) // cmd->size includes null terminator
        return 0;
    return strncmp(cmd->buffer, string, strlen(string)) == 0;
}

int command_matches_exactly(command* cmd, const char* string)
{
    if (cmd->size <= strlen(string)) // cmd->size includes null terminator
        return 0;
    if (cmd->size > (strlen(string) + 1)) return 0;
    return strncmp(cmd->buffer, string, strlen(string)) == 0;
}

int command_match_builtins(command* cmd)
{
    if (command_matches(cmd, "exit ")) { exit(atoi(cmd->buffer + 5)); }
    if (command_matches_exactly(cmd, "exit")) { exit(0); }
    if (command_matches_exactly(cmd, "pid"))
    {
        printf("pid %ld, ppid %ld\n", getpid(), getppid());
        return 1;
    }
    return 0;
}

void command_expand(command* cmd, long new_capacity)
{
    char* buffer = realloc(cmd->buffer, new_capacity);
    if (!buffer)
    {
        perror("realloc");
        exit(1);
    }
    cmd->buffer = buffer;
    cmd->capacity = new_capacity;
}

void command_push(command* cmd, char c)
{
    if (cmd->size == cmd->capacity) command_expand(cmd, cmd->capacity + 8);
    cmd->buffer[cmd->size] = c;
    cmd->size++;
}

void command_pop(command* cmd)
{
    cmd->size--;
}

void command_init(command* cmd)
{
    cmd->buffer = malloc(5);
    cmd->capacity = 5;
    cmd->size = 0;
}

void command_clear(command* cmd)
{
    free(cmd->buffer);
    return command_init(cmd);
}

void command_execute(command* cmd)
{
    command_push(cmd, '\0');
    if (command_match_builtins(cmd))
    {
        command_clear(cmd);
        show_prompt();
        return;
    }
    pid_t child = fork();
    if (child < 0)
    {
        perror(cmd->buffer);
        command_clear(cmd);
        show_prompt();
        return;
    }
    if (child == 0)
    {
        if (cmd->buffer[0] != '/' && access(cmd->buffer, F_OK) < 0) // FIXME: Race condition.
        {
            if (errno == ENOENT)
            { // Try in /bin
                char* buf = malloc(cmd->size + 6);
                strlcpy(buf, "/bin/", 6);
                strncat(buf, cmd->buffer, cmd->size);
                execv(buf, NULL);
            }
        }
        else
            execv(cmd->buffer, NULL);
        perror(cmd->buffer);
        exit(127);
    }
    pid_t result;
    while ((result = waitpid(child, &status, 0)) == 0) { msleep(20); }
    if (result < 0)
    {
        perror("waitpid");
        command_clear(cmd);
        show_prompt();
        return;
    }
    int exit_status = WEXITSTATUS(status);
    if (exit_status == -2 || exit_status == -3) printf("(PID %ld) Segmentation fault\n", result);
    command_clear(cmd);
    show_prompt();
}

void command_concat_char(command* cmd, char c)
{
    if (c == '\b')
    {
        if (cmd->size != 0)
        {
            putchar(c);
            command_pop(cmd);
        }
    }
    else if (c == '\n')
    {
        if (cmd->size == 0)
        {
            putchar(c);
            show_prompt();
        }
        else
        {
            putchar(c);
            command_execute(cmd);
        }
    }
    else
    {
        putchar(c);
        command_push(cmd, c);
    }
}

void command_concat(command* cmd, const char* str)
{
    while (*str)
    {
        command_concat_char(cmd, *str);
        str++;
    }
}

int main()
{
    show_prompt();

    command shell_command;
    command_init(&shell_command);

    while (1)
    {
        int c = getchar();
        if (c == EOF)
        {
            if (ferror(stdin))
            {
                perror("getchar");
                return 1;
            }
            if (feof(stdin)) { return 0; }
            assert(false); // we should never get here
        }
        command_concat_char(&shell_command, (char)c);
    }
}