/*
 * 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/wnix.h>
#include <bin/proc.h>
#include <bin/routines.h>
#include <bin/types.h>
#include <bin/wnix/routines.h>
#include <aio.h>
#include <fs/fs.h>
#include <loop.h>
#include <nanowasm/nw.h>
#include <nanowasm/linear.h>
#include <errno.h>
#include <stdlib.h>

struct write
{
    long fd;
    int error;
    unsigned long addr, count, errno_addr;
    char buf[64];
    size_t i;
    union nw_value *ret;
    struct aio *aio;
    struct bin_proc_fd *pfd;
    struct nw_sm_io io;
};

static enum nw_state read_chunk(void *, struct nw_next *);

static void free_write(struct write *const w)
{
    if (!w)
        return;

    aio_free(w->aio);
    free(w);
}

static enum nw_state fail(void *const user, struct nw_next *const next)
{
    struct bin_proc *const p = user;
    struct write *const w = p->import;

    w->ret->i32 = -1;
    free_write(w);
    return NW_OK;
}

static enum nw_state set_errno(struct bin_proc *const p, const int error,
    struct nw_next *const next)
{
    struct write *const w = p->import;

    if (w->errno_addr)
    {
        const struct bin_set_errno cfg =
        {
            .addr = w->errno_addr,
            .error = error,
            .p = p
        };

        *next = (const struct nw_next)
        {
            .fn = fail,
            .user = p
        };

        if (bin_wnix_set_errno(&cfg, next))
        {
            free_write(w);
            return NW_FATAL;
        }
    }
    else
        return fail(p, next);

    return NW_AGAIN;
}

static void setup_buf(struct bin_proc *const p, struct nw_next *const next)
{
    struct write *const w = p->import;
    const size_t rem = w->count - w->i,
        n = rem > sizeof w->buf ? sizeof w->buf : rem;

    w->io = (const struct nw_sm_io)
    {
        .buf = w->buf,
        .n = n
    };

    *next = (const struct nw_next){.fn = read_chunk, .user = p};
}

static enum nw_state check_write(void *const user, struct nw_next *const next)
{
    struct bin_proc *const p = user;
    struct write *const w = p->import;

    if (w->error)
    {
        if (set_errno(p, w->error, next))
        {
            free_write(w);
            return NW_FATAL;
        }
    }
    else if ((w->i += w->io.n) >= w->count)
    {
        w->ret->i32 = w->count;
        free_write(w);
        return NW_OK;
    }
    else
        setup_buf(p, next);

    return NW_AGAIN;
}

static int write_done(const enum state state, void *const args)
{
    struct bin_proc *const p = args;
    struct write *const w = p->import;

    w->error = state ? errno : 0;

    if (bin_proc_unlock(p) || loop_rm_aio(w->aio))
    {
        free_write(w);
        return -1;
    }

    aio_free(w->aio);
    w->aio = NULL;
    return 0;
}

static enum nw_state do_write(void *const user, struct nw_next *const next)
{
    struct bin_proc *const p = user;
    struct write *const w = p->import;
    const struct aio_done d =
    {
        .f = write_done,
        .args = user
    };

    const struct fs_write fw =
    {
        .fd = &w->pfd->fs_fd,
        .buf = w->buf,
        .n = w->io.n
    };

    struct aio *const aio = aio_write(&fw, &d);

    if (!aio || bin_proc_lock(p) || loop_add_aio(aio))
    {
        aio_free(aio);
        free_write(w);
        return NW_FATAL;
    }

    w->aio = aio;
    *next = (const struct nw_next){.fn = check_write, .user = user};
    return NW_AGAIN;
}

static enum nw_state read_chunk(void *const user, struct nw_next *const next)
{
    struct bin_proc *const p = user;
    struct write *const w = p->import;
    const unsigned long offset = w->addr + w->i;
    const enum nw_state n = nw_linear_load(&p->instance, &w->io, offset);

    switch (n)
    {
        case NW_OK:
            *next = (const struct nw_next){.fn = do_write, .user = user};
            break;

        case NW_FATAL:
            free_write(w);
            return n;

        case NW_AGAIN:
            break;
    }

    return NW_AGAIN;
}

static enum nw_state check(void *const user, struct nw_next *const next)
{
    struct bin_proc *const p = user;
    struct write *const w = p->import;

    if (!(w->pfd = bin_proc_fd_find(p, w->fd)))
        return set_errno(p, EBADF, next);
    else if (!w->addr || !w->errno_addr)
        return set_errno(p, EINVAL, next);
    else
        setup_buf(p, next);

    return NW_AGAIN;
}

enum nw_state bin_wnix_write(const union nw_value *const params,
    union nw_value *const ret, void *const user,
    struct nw_next *const next)
{
    enum {FD, ADDR, COUNT, ERRNO};
    struct write *const w = malloc(sizeof *w);

    if (!w)
        return NW_FATAL;

    *w = (const struct write)
    {
        .fd = params[FD].i32,
        .addr = params[ADDR].i32,
        .count = params[COUNT].i32,
        .errno_addr = params[ERRNO].i32,
        .ret = ret
    };

    struct bin_proc *const p = user;

    *next = (const struct nw_next){.fn = check, .user = user};
    p->import = w;
    return NW_AGAIN;
}
