#define SHELL_OPTS_ERR_INVALID_OPT -1
#define SHELL_OPTS_ERR_EXTRA_OPD -2

U0 @shell_free_args(I64 argc, U8** argv)
{
    I64 i;
    for (i = 0; i < argc; i++)
        Free(argv[i]);
    if (argv)
        Free(argv);
}

U8** @shell_parse_args(@shell* sh, U8* str,
    I64* argc)
{ // Return argc, argv from str.
    Bool quoted = FALSE;
    I64 _argc = 0;
    U8** _argv = NULL;
    U8** _tmp = CAlloc(sizeof(U64) * StrLen(str));
    I64 i = 0;
    I64 s = 0;
    I64 len;
    while (i < StrLen(str) + 1) {
        switch (str[i]) {
        case 0:
        case ' ':
            if (!quoted) {
                len = (str + i) - (str + s - 1);
                if (str[i - 1] == '"')
                    len--;
                if (len - 1) {
                    _tmp[_argc] = CAlloc(len);
                    MemCpy(_tmp[_argc], str + s, len - 1);
                    _argc++;
                }
                s = i + 1;
            }
            break;
        case '"':
            quoted = !quoted;
            if (quoted)
                s = i + 1;
            break;
        default:
            break;
        }
        i++;
    }
    *argc = _argc;
    _argv = CAlloc(sizeof(U64) * _argc);
    MemCpy(_argv, _tmp, sizeof(U64) * _argc);
    Free(_tmp);
    return _argv;
}

I64 @shell_parse_opts(@shell* sh, U8* op_lst, I64 argc, U8** argv, I64* flags,
    U64* op_err, Bool ignore_extra_opd = FALSE)
{
    I64 i, j;
    U8 op_chr[2];
    op_chr[1] = 0;
    for (i = 1; i < argc; i++) {
        if (argv[i][0] == '-') {
            for (j = 1; j < StrLen(argv[i]); j++) {
                op_chr[0] = argv[i][j];
                if (StrFind(&op_chr, op_lst))
                    *flags |= 1 << (StrFind(&op_chr, op_lst) - op_lst);
                else {
                    *op_err = StrNew(&op_chr);
                    return SHELL_OPTS_ERR_INVALID_OPT;
                }
            }
        } else {
            if (!ignore_extra_opd) {
                *op_err = StrNew(argv[i]);
                return SHELL_OPTS_ERR_EXTRA_OPD;
            }
        }
    }
    return 0;
}

U8* @shell_expand_relative_path_to_root_path(@shell* sh, U8* path)
{
    switch (path[0]) {
    case '/':
        return StrNew(path);
        break;
    default:
        U8* abs_path = CAlloc(StrLen(path) + StrLen(&sh->cwd) + 4);
        StrPrint(abs_path, "%s/%s", &sh->cwd, path);
        return abs_path;
        break;
    }
}

U8* @shell_expand_relative_path(@shell* sh, U8* path)
{
    if (!path || !sh)
        return NULL;
    if (StrLen(path) < 1)
        return NULL;
    U8* relative_path_to_root_path = @shell_expand_relative_path_to_root_path(sh, path);
    if (!MemCmp(relative_path_to_root_path, "/mnt/", 5))
        return relative_path_to_root_path;
    if (!MemCmp(relative_path_to_root_path, "/sys/", 5))
        return relative_path_to_root_path;
    U8* prepend_root_path = config->o("default")->o("filesystem")->@("root");
    U8* full_expanded_path = CAlloc(StrLen(relative_path_to_root_path) + StrLen(prepend_root_path) + 8);
    StrPrint(full_expanded_path, "%s%s", prepend_root_path, relative_path_to_root_path);
    Free(relative_path_to_root_path);
    return full_expanded_path;
}

U8* @shell_get_env_var(@shell* sh, U8* key)
{
    @shell_env_var* var = sh->env->next;
    while (var) {
        if (StrLen(&var->key) && StrLen(&var->value))
            if (!StrCmp(&var->key, key))
                return &var->value;
        var = var->next;
    }
    return "";
}

U0 @shell_set_env_var(@shell* sh, U8* key, U8* value)
{
    if (!sh || !key || !value)
        return;
    @shell_env_var* var = sh->env->next;
    while (var->next) {
        if (!StrCmp(&var->key, key)) {
            StrCpy(&var->value, value);
            return;
        }
        var = var->next;
    }
    @shell_env_var* new = CAlloc(sizeof(@shell_env_var));
    StrCpy(&new->key, key);
    StrCpy(&new->value, value);
    new->prev = var;
    var->next = new;
}

U0 @shell_unset_env_var(@shell* sh, U8* key)
{
    @shell_env_var* var = sh->env->next;
    @shell_env_var* prev = NULL;
    @shell_env_var* next = NULL;
    while (var) {
        if (!StrCmp(&var->key, key)) {
            prev = var->prev;
            next = var->next;
            if (prev)
                prev->next = next;
            if (next)
                next->prev = prev;
            Free(var);
            return;
        }
        var = var->next;
    }
}

U0 @shell_history_append(@shell* sh, U8* str)
{
    if (!sh || !str)
        return;
    if (!StrCmp(str, ""))
        return;
    I64 i;
    sh->history.entries[sh->history.pos] = StrNew(str);
    sh->history.pos++;
    if (sh->history.pos > SHELL_HISTORY_LIMIT - 1) {
        Free(sh->history.entries[0]);
        for (i = 0; i < SHELL_HISTORY_LIMIT; i++)
            sh->history.entries[i] = sh->history.entries[i + 1];
        sh->history.pos--;
    }
}

U0 @shell_process_args(@shell* sh, I64 argc, U8** argv)
{
    if (argc < 1 || !argv)
        return;
    I64 i;
    U8 buf[256];
    for (i = 0; i < argc; i++) {
        if (argv[i][0] == '\d') {
            switch (argv[i][1]) {
            case '?':
                Free(argv[i]);
                StrPrint(&buf, "%d", sh->answer);
                argv[i] = StrNew(&buf);
                break;
            case '0':
                Free(argv[i]);
                argv[i] = StrNew("esh");
                break;
            default:
                Free(argv[i]);
                argv[i] = StrNew(@shell_get_env_var(sh, argv[i] + 1));
                break;
            }
        }
        if (!StrCmp(argv[i], "~")) {
            Free(argv[i]);
            argv[i] = StrNew(&sh->session->home);
        }
    }
}

U0 @shell_update_prompts(@shell* sh)
{
    U8 buf[512];
    U8 buf2[512];
    StrCpy(&buf, &sh->cwd);
    StrPrint(buf2, "/home/%s", &sh->session->user.name);
    if (!StrCmp(&buf, &buf2))
        StrCpy(&buf, "~");
    StrPrint(&sh->PS1, "\x1b[0;32m[%s@%s %s]\x1b[1;37m\d ", &sh->session->user.name,
        &sh->session->hostname, &buf);
}

U0 @shell_set_window_title(@shell* sh)
{
    U8 buf[512];
    U8 buf2[512];
    U8 buf3[512];
    StrCpy(&buf, &sh->cwd);
    StrPrint(buf2, "/home/%s", &sh->session->user.name);
    if (!StrCmp(&buf, &buf2))
        StrCpy(&buf, "~");
    StrPrint(buf3, "[%s@%s %s]", &sh->session->user.name,
        &sh->session->hostname, &buf);
    Stdio.WriteLine(sh, "\x1b]0;");
    Stdio.WriteLine(sh, &buf3);
    Stdio.WriteLine(sh, "\x07");
}

I64 @shell_input_loop(@shell* sh)
{
    CHashFun* cmd;
    I64 argc;
    U8** argv;
    U8 buf[4096];
    Bool exit = FALSE;
    I64 i;
    I64 (*@shell_exec)(@shell* sh, I64 argc, U8** argv);

    while (!exit) {

        @shell_update_prompts(sh);
        @shell_set_window_title(sh);
        Stdio.ReadLine(sh, sh->PS1, buf);

        @shell_history_append(sh, &buf);
        argv = @shell_parse_args(sh, &buf, &argc);

        if (argc) {
            if (!StrCmp(argv[0], "exit")) {
                exit = TRUE;
                goto @shell_exit;
            }
            @shell_process_args(sh, argc, argv);
            StrPrint(&buf, "@shell_cmd_%s", argv[0]);
            cmd = HashFind(&buf, adam_task->hash_table, HTT_FUN);
            if (cmd) {
                @shell_exec = cmd->exe_addr;
                sh->answer = @shell_exec(sh, argc, argv);
                sh->break = FALSE;
                FifoU8Flush(sh->input);
            } else {
                StrPrint(&buf, "%s: command not found\n", argv[0]);
                Stdio.WriteLine(sh, &buf);
                sh->answer = 0;
            }
        }

        @shell_exit : @shell_free_args(argc, argv);
    }
    return 0;
}

U0 @shell_instance(@shell* sh)
{
    @shell_input_loop(sh);
    sh->exit = TRUE;
}

U0 @shell_init(@shell* sh)
{
    sh->env = CAlloc(sizeof(@shell_env_var));
    sh->history.limit = SHELL_HISTORY_LIMIT;
    sh->history.pos = 0;
    sh->history.entries = CAlloc(sizeof(U64) * SHELL_HISTORY_LIMIT);
    sh->input = FifoU8New(SHELL_INPUT_FIFO_SIZE);
    sh->task = Spawn(&@shell_instance, sh);
}

@shell* @shell_new(Bool headless = FALSE)
{
    @shell* sh = CAlloc(sizeof(@shell));
    if (!headless)
        @shell_init(sh);
    StrCpy(&sh->cwd, &Compositor.session.home);
    return sh;
}

"shell ";