/*
 * 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 <init.h>
#include <init/ps1/types.h>
#include <aio.h>
#include <bin.h>
#include <fs/fs.h>
#include <iso9660.h>
#include <kprintf.h>
#include <loop.h>
#include <net.h>
#include <state.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

enum {RETRIES = 5};
static const char serial[] = "/dev/ttyS0";
static const struct stream
{
    const char *path;
    int flags;
} streams[] =
{
    {.path = "/dev/null", .flags = O_RDONLY},
    {.path = "/dev/tty", .flags = O_WRONLY},
    {.path = "/dev/tty", .flags = O_WRONLY}
};

static int retry_cd(struct init_ps1 *);
static int open_stream(struct init_ps1 *p);

static int exec(struct init_ps1 *const p)
{
    const char *const args[] = {NULL};

    kprintf("Opening second-stage initd\n");
    p->next = NULL;

    /* TODO: create symbolic from /mnt/cdrom/bin to /bin */
    return bin_exec("/mnt/cdrom/bin/initd", args, &p->ss, 0, 0, 0);
}

static int stream_opened(const enum state state, void *const args)
{
    struct init_ps1 *const p = args;
    const struct stream *const s = &streams[p->stream_i];

    if (loop_rm_aio(p->aio))
        return -1;

    aio_free(p->aio);

    if (state)
        kprintf("Failed to open %s: %s\n", s->path, strerror(errno));
    else
    {
        kprintf("Successfully opened %s\n", s->path);

        if (++p->stream_i >= sizeof streams / sizeof *streams)
            p->next = exec;
        else
            p->next = open_stream;
    }

    return 0;
}

static int open_stream(struct init_ps1 *const p)
{
    const struct stream *const s = &streams[p->stream_i];
    const struct fs_open o =
    {
        .fd = &p->ss.streams[p->stream_i],
        .path = s->path,
        .flags = s->flags
    };

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

    struct aio *const aio = aio_open(&o, &d);

    if (!aio || loop_add_aio(aio))
        return -1;

    p->aio = aio;
    p->next = NULL;
    return 0;
}

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

    if (loop_rm_aio(p->aio))
        return -1;

    aio_free(p->aio);

    if (state)
        kprintf("Failed to mount ISO9660 filesystem\n");
    else
    {
        kprintf("ISO9660 filesystem mounted successfully\n");
        p->next = open_stream;
    }

    return 0;
}

static int mount_cd(struct init_ps1 *const p)
{
    struct aio *aio = NULL;
    const struct aio_mount fm =
    {
        .mount =
        {
            .src = "/dev/cd0",
            .tgt = "/mnt/cdrom",
            .mode = 0755
        },

        .type = "iso9660"
    };

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

    if (!(aio = aio_mount(&fm, &d)) || loop_add_aio(aio))
        goto failure;

    p->next = NULL;
    p->aio = aio;
    kprintf("Mounting ISO9660 filesystem from %s to %s\n",
        fm.mount.src, fm.mount.tgt);
    return 0;

failure:
    kprintf("Failed to mount ISO9660 filesystem\n");
    aio_free(aio);
    return -1;
}

static int wait_retry(struct init_ps1 *const p)
{
    struct timespec ts;
    const time_t last = p->last_retry.tv_sec, timeout = 5;

    if (clock_gettime(CLOCK_REALTIME, &ts))
        return -1;
    else if (ts.tv_sec - last >= timeout)
        p->next = p->retry;

    return 0;
}

static int stat_done(const enum state s, void *const args)
{
    int ret = 0;
    struct init_ps1 *const p = args;

    if (s)
    {
        struct timespec ts;

        if (!--p->retries || clock_gettime(CLOCK_REALTIME, &ts))
        {
            ret = -1;
            goto end;
        }

        p->last_retry = ts;
        p->next = wait_retry;
    }
    else
    {
        p->next = p->success;
        kprintf("Successfully initialized %s (took %u attempts)\n",
            p->path, RETRIES - p->retries + 1);
    }

end:
    loop_rm_aio(p->aio);
    aio_free(p->aio);
    free(p->sb);
    return ret;
}

static int retry_cd(struct init_ps1 *const p)
{
    static const char path[] = "/dev/cd0";
    struct aio *aio = NULL;
    struct stat *const sb = malloc(sizeof *sb);

    if (!sb)
        goto failure;

    const struct fs_stat s = {.path = path, .sb = sb};
    const struct aio_done d = {.f = stat_done, .args = p};
    const unsigned retries = p->retries;

    if (!(aio = aio_stat(&s, &d)) || loop_add_aio(aio))
        goto failure;

    *p = (const struct init_ps1)
    {
        .aio = aio,
        .sb = sb,
        .path = path,
        .retries = retries,
        .success = mount_cd,
        .last_retry = {.tv_sec = (time_t)-1}
    };

    p->next = NULL;
    kprintf("Initializing CD-ROM to %s (attempt %u)\n", path,
        RETRIES - p->retries + 1);
    return 0;

failure:
    free(aio);
    free(sb);
    return -1;
}

static int init_net(struct init_ps1 *const p)
{
    if (net_init(serial))
        return -1;

    p->next = retry_cd;
    return 0;
}

static int retry_serial(struct init_ps1 *const p)
{
    struct aio *aio = NULL;
    struct stat *const sb = malloc(sizeof *sb);

    if (!sb)
        goto failure;

    const struct fs_stat s = {.path = serial, .sb = sb};
    const struct aio_done d = {.f = stat_done, .args = p};
    const unsigned retries = p->retries;

    if (!(aio = aio_stat(&s, &d)) || loop_add_aio(aio))
        goto failure;

    *p = (const struct init_ps1)
    {
        .aio = aio,
        .sb = sb,
        .path = serial,
        .retries = retries,
        .success = init_net,
        .last_retry = {.tv_sec = (time_t)-1}
    };

    p->next = NULL;
    kprintf("Initializing serial device to %s (attempt %u)\n", serial,
        RETRIES - p->retries + 1);
    return 0;

failure:
    free(aio);
    free(sb);
    return -1;
}

int init_port_boot(void)
{
    if (iso9660_register())
        return -1;

    init_ps1 = (const struct init_ps1)
    {
        .retries = RETRIES,
        .next = retry_serial
    };

    return 0;
}
