/*
 * wnix, a Unix-like operating system for WebAssembly applications.
 * Copyright (C) 2025  Xavier Del Campo Romero
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <bin.h>
#include <bin/mod.h>
#include <bin/proc.h>
#include <bin/routines.h>
#include <bin/types.h>
#include <aio.h>
#include <caio.h>
#include <fs/fs.h>
#include <loop.h>
#include <kprintf.h>
#include <dynstr.h>
#include <nanowasm/nw.h>
#include <nanowasm/dbg.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>

struct exec
{
    struct bin_mod *m;
    struct bin_proc *p;
};

static int announced(const enum state state, void *const args)
{
    struct bin_proc *const p = args;
    struct bin_dbg *const d = &p->dbg;
    const struct nw_dbg_cfg cfg = {.inst = &p->instance};

    dynstr_free(&d->dstr);

    if (loop_rm_aio(d->aio))
        goto failure;

    aio_free(d->aio);

    if (state)
        kprintf("failed to announce process to debugger: %s\n", p->path);
    else
        nw_dbg_init(&d->dbg, &cfg);

    return 0;

failure:
    bin_proc_free(p);
    return -1;
}

static int announce(struct bin_proc *const p)
{
    struct aio *aio = NULL;
    struct bin_dbg *const dbg = &p->dbg;
    struct dynstr *const s = &dbg->dstr;
    const struct aio_done d =
    {
        .f = announced,
        .args = p
    };

    dynstr_init(s);

    if (dynstr_append(s, ";hi:%s:%u\n", p->path, (unsigned)p->pid))
        goto failure;

    const struct fs_write w =
    {
        .buf = s->str,
        .n = s->len + 1,
        .fd = &dbg->fd
    };

    if (!(aio = aio_write(&w, &d)) || loop_add_aio(aio))
        goto failure;

    dbg->aio = aio;
    return 0;

failure:
    aio_free(aio);
    return -1;
}

static int mod_opened(const enum state state, void *const args)
{
    struct exec *const e = args;
    struct bin_mod *const m = e->m;
    struct bin_proc *const p = e->p;
    struct nw_mod_cfg cfg = bin_mod_cfg;

    cfg.io.user = m;
    cfg.imp_indexes = m->import_indexes;
    nw_init(&m->mod, &cfg);

    if (state)
        goto failure;
    else if (bin_load(m)
        || bin_proc_init(p)
        || announce(p))
        goto failure;

    return 0;

failure:
    bin_mod_free(m);
    return -1;
}

static int init_mod(struct bin_proc *const p)
{
    struct caio *caio = NULL;
    char *const pathdup = strdup(p->path);
    struct exec *const e = malloc(sizeof *e);
    struct bin_mod *const m = malloc(sizeof *m);
    const struct caio_open op =
    {
        .flags = O_RDONLY,
        .path = p->path,
        .gid = p->gid,
        .uid = p->uid
    };

    const struct aio_done d =
    {
        .f = mod_opened,
        .args = e,
    };

    if (!pathdup || !e || !m || !(caio = caio_open(&op, &d)))
        goto failure;

    *m = (const struct bin_mod)
    {
        .path = pathdup,
        .caio = caio
    };

    *e = (const struct exec)
    {
        .m = m,
        .p = p
    };

    p->mod = m;
    return 0;

failure:
    caio_close(caio);
    free(pathdup);
    free(m);
    free(e);
    return -1;
}

static int header_read(struct bin_proc *const p)
{
    static const char shebang[] = "#! ", wasm[] = {'\0', 'a', 's', 'm'};
    const char *const h = p->header;

    if (!strncmp(h, shebang, strlen(shebang)))
    {
        /* TODO: interpret shebang */
    }
    else if (!memcmp(h, wasm, sizeof wasm))
    {
        if (init_mod(p))
            goto failure;
    }
    else
    {
        kprintf("Invalid file signature: {%hhx, %hhx, %hhx, %hhx}\n",
            h[0], h[1], h[2], h[3]);
        goto failure;
    }

    if (caio_seek(p->caio, 0))
        goto failure;

    return 0;

failure:
    bin_proc_free(p);
    return -1;
}

static int read_header(const enum state state, void *const args)
{
    struct bin_proc *const p = args;

    if (state)
        goto failure;

    const size_t rem = sizeof p->header - p->i;
    const struct aio_done d = {.f = read_header, .args = p};
    const int n = caio_read(p->caio, p->header + p->i, rem, &d);

    if (n < 0)
        goto failure;
    else if ((p->i += n) >= sizeof p->header)
        return header_read(p);

    return 0;

failure:
    bin_proc_free(p);
    return -1;
}

static int opened(const enum state state, void *const args)
{
    struct bin_proc *const p = args;

    if (state)
        goto failure;
    else if ((p->mod = bin_mod(p->path)))
        return bin_proc_init(p);

    p->i = 0;
    return read_header(STATE_OK, p);

failure:
    bin_proc_free(p);
    return -1;
}

static int serial_opened(const enum state state, void *const args)
{
    struct bin_proc *const p = args;
    struct bin_dbg *const dbg = &p->dbg;
    struct caio *caio = NULL;
    const struct aio_done d =
    {
        .f = opened,
        .args = p
    };

    const struct caio_open op =
    {
        .flags = O_RDONLY,
        .path = p->path,
        .uid = p->uid,
        .gid = p->gid
    };

    if (loop_rm_aio(dbg->aio))
        goto failure;

    aio_free(dbg->aio);
    dbg->aio = NULL;

    if (state
        || !(caio = caio_open(&op, &d))
        || caio_set_caches(caio, 3))
        goto failure;

    p->caio = caio;
    return 0;

failure:
    caio_close(caio);
    bin_proc_free(p);
    return -1;
}

static int copy_args(struct bin_proc *const p, const char *const path,
    const char *const *const argv)
{
    int nargs = 2;
    const char *const *parg = argv;
    char **args = NULL;

    while (*parg++)
        nargs++;

    if (!(args = malloc(nargs * sizeof *args)))
        goto failure;

    for (int i = 0; i < nargs; i++)
        args[i] = NULL;

    if (!(*args = strdup(path)))
        goto failure;

    for (int i = 1; i < nargs - 1; i++)
        if (!(args[i] = strdup(*(argv - 1))))
            goto failure;

    p->argc = nargs - 1;
    p->argv = args;
    return 0;

failure:

    if (args)
        for (int i = 0; i < nargs; i++)
            free(args[i]);

    free(args);
    return -1;
}

int bin_exec(const char *const path, const char *const *const argv,
    const struct fs_stdstreams *const ss, const pid_t parent, const uid_t uid,
    const gid_t gid)
{
    const pid_t pid = bin_pid();
    char *pathdup = NULL;
    struct aio *serial = NULL;
    struct bin_proc *const p = malloc(sizeof *p);

    if (!p)
        goto failure;

    struct bin_dbg *const dbg = &p->dbg;

    const struct aio_done d =
    {
        .f = serial_opened,
        .args = p
    };

    const struct fs_open op =
    {
        .path = "/dev/ttyS0",
        .flags = O_RDWR,
        .fd = &dbg->fd,
        .uid = uid,
        .gid = gid
    };

    if (!(pathdup = strdup(path))
        || !(serial = aio_open(&op, &d)))
        goto failure;

    *p = (const struct bin_proc)
    {
        .path = pathdup,
        .dbg.aio = serial,
        .parent = parent,
        .pid = pid,
        .uid = uid,
        .gid = gid,
        .ss = *ss,
        .umask = S_IWGRP | S_IWOTH
    };

    if (copy_args(p, path, argv)
        || loop_add_aio(serial))
        goto failure;

    return 0;

failure:
    aio_free(serial);
    free(pathdup);
    free(p);
    return -1;
}
