/*
 * 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 <iso9660.h>
#include <iso9660/ops.h>
#include <iso9660/routines.h>
#include <iso9660/types.h>
#include <fs/fs.h>
#include <kprintf.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct id
{
    char buf[UCHAR_MAX];
};

struct search
{
    union inode_result *inode;
    struct fs_mp src_mp;
    struct iso9660_entry entry;
    const char *src, *path;
    struct fs_ret r, *pr;
    struct fs_fd fd;
    off_t offset;
    size_t token_i;
    char *token;
    void *buf;
    struct id id;
};

static int prepare_entry(struct search *);
static int next_entry(struct search *);

static void free_search(struct search *const s)
{
    if (!s)
        return;

    const struct fs *const fs = s->src_mp.fs;

    if (fs)
        fs->close(&s->fd);

    free(s->buf);
    free(s->token);
    free(s);
}

static void dealloc(void *const p)
{
    free(p);
}

static int set_inode(struct search *const s,
    const struct iso9660_entry *const e, const struct id *const id)
{
    struct inode *const inode = &s->inode->cachei;
    struct inode_prv *p = NULL;
    char *namedup = NULL;
    struct timespec ts, atim;

    if (clock_gettime(CLOCK_REALTIME, &atim))
        goto failure;
    else if (iso9660_etotimespec(&e->dt, &ts))
    {
        kprintf("Invalid datetime, path=%s\n", s->path);
        goto failure;
    }
    else if (!(namedup = strdup(id->buf)) || !(p = malloc(sizeof *p)))
        goto failure;

    *p = (const struct inode_prv)
    {
        .offset = iso9660_lmsb32(&e->extent_loc) * ISO9660_SECTOR_SZ
    };

    *inode = (const struct inode)
    {
        .size = iso9660_lmsb32(&e->data_len),
        .flags = INODE_REGFILE,
        .name = namedup,
        .mode = 0555,
        .atim = atim,
        .dealloc = dealloc,
        .ctim = ts,
        .mtim = ts,
        .prv = p
    };

    return 0;

failure:
    free(p);
    free(namedup);
    return -1;
}

static int next_token(struct search *const s)
{
    free(s->token);
    s->token = NULL;

    if (fs_next(s->path, &s->token, &s->token_i))
    {
        kprintf("Failed to extract next token, path=%s, i=%zu\n", s->path,
            s->token_i);
        return -1;
    }

    return 0;
}

static int check_version(const char *const version)
{
    char *end;

    errno = 0;
    strtoul(version, &end, 10);

    return errno || *end;
}

static void extract_id(const char *id, struct id *const out,
    const char *const ext, const char *const version)
{
    char *p = out->buf;

    if (ext)
    {
        const ptrdiff_t n = ext - id;
        const char *const e = ext + 1;

        memcpy(p, id, n);
        p += n;
        id += n;

        if (*e)
        {
            const ptrdiff_t n = version ? version - e : strlen(e);

            memcpy(p, id, n);
            p += n;
        }
    }
    else if (version)
    {
        memcpy(p, id, version - id);
        p += version - id;
    }
    else
    {
        const ptrdiff_t n = strlen(id);

        memcpy(p, id, n);
        p += n;
    }

    *p = '\0';
    p = out->buf;

    while (*p)
    {
        *p = tolower(*p);
        p++;
    }
}

static int parse_id(const char *const id, struct id *const out)
{
    static const char accept[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
    const char *const ext = strchr(id, '.'), *const version = strchr(id, ';');
    const size_t n = strspn(id, accept), len = strlen(id);

    /* Ensure only valid characters are met if no version or extension. */
    if (!version && !ext && n != len)
        return -1;
    /* Ensure file version appears only after file extension,
     * if both are present. */
    else if (version && ext && ext > version)
        return -1;
    /* Ensure one and only one file version and/or extension appear. */
    else if ((version && strchr(version + 1, ';'))
        || (ext && strchr(ext + 1, '.')))
        return -1;
    else if (version && check_version(version + 1))
        return -1;
    /* When present, ensure valid characters only until the file extension
     * is found. */
    else if (ext && n != ext - id)
        return -1;

    extract_id(id, out, ext, version);
    return 0;
}

static enum state id_read(void *const args)
{
    struct search *volatile const s = args;
    const struct iso9660_entry *const e = &s->entry;
    const char *const id = s->id.buf;
    struct id outid;

    if (!*id || !strcmp(id, "\1"))
    {
        if (next_entry(s))
            goto failure;
    }
    else if (parse_id(id, &outid))
    {
        kprintf("Invalid file or directory identifier=%s\n", id);
        goto failure;
    }
    else if (!strcmp(outid.buf, s->token))
    {
        char *token;

        if (fs_next(s->path, &token, &s->token_i))
        {
            kprintf("Failed to extract next token, path=%s, i=%zu\n",
                s->path, s->token_i);
            goto failure;
        }
        else if (!token)
        {
            if (set_inode(s, e, &outid))
                goto failure;

            *s->pr = s->r;
            free_search(s);
            return STATE_AGAIN;
        }
        else if (!(e->flags & ISO9660_FLAGS_DIR))
        {
            kprintf("Intermediate node %s not a directory, path=%s\n",
                id, s->path);
            goto failure;
        }
        else
        {
            s->fd.offset = iso9660_lmsb32(&e->extent_loc) * ISO9660_SECTOR_SZ;

            if (prepare_entry(s))
                goto failure;
        }

        free(s->token);
        free(s->buf);
        s->token = token;
        s->buf = NULL;
    }
    else if (next_entry(s))
        goto failure;

    return STATE_AGAIN;

failure:
    free_search(s);
    return STATE_FATAL;
}

static enum state entry_read(void *const args)
{
    struct search *const s = args;
    const struct iso9660_entry *const e = &s->entry;

    if (e->len < sizeof *e)
    {
        kprintf("Unexpected entry length %hhu, expected %zu\n", e->len,
            sizeof *e);
        goto failure;
    }

    char *const buf = s->id.buf;
    const struct fs_mp *const mp = &s->fd.mp;
    const struct fs_read r =
    {
        .buf = buf,
        .n = e->id_len,
        .fd = &s->fd
    };

    *(buf + e->id_len) = '\0';
    *s->pr = (const struct fs_ret)
    {
        .f = id_read,
        .args = s
    };

    if (mp->fs->read(&r, s->pr))
        goto failure;

    return STATE_AGAIN;

failure:
    free_search(s);
    return STATE_FATAL;
}

static int prepare_entry(struct search *const s)
{
    const struct fs_read r =
    {
        .buf = &s->entry,
        .fd = &s->fd,
        .n = sizeof s->entry
    };

    *s->pr = (const struct fs_ret)
    {
        .f = entry_read,
        .args = s
    };

    s->offset = s->fd.offset;
    return s->src_mp.fs->read(&r, s->pr);
}

static int next_entry(struct search *const s)
{
    const struct iso9660_entry *const e = &s->entry;
    struct fs_fd *const fd = &s->fd;

    fd->offset = s->offset + e->len;
    return prepare_entry(s);
}

static int set_root_inode(const struct iso9660_pvd *const pvd,
    struct search *const s)
{
    struct inode *const inode = &s->inode->cachei;
    struct inode_prv *p = NULL;
    char *namedup = NULL;

    if (clock_gettime(CLOCK_REALTIME, &inode->atim))
        goto failure;
    else if (iso9660_totimespec(&pvd->creat, &inode->ctim))
    {
        kprintf("Invalid volume creation time\n");
        goto failure;
    }
    else if (iso9660_totimespec(&pvd->mod, &inode->mtim))
    {
        kprintf("Invalid volume modification time\n");
        goto failure;
    }
    else if (!(namedup = strdup("/")) || !(p = malloc(sizeof *p)))
        goto failure;

    *p = (const struct inode_prv)
    {
        .offset = offsetof(struct iso9660_pvd, root_dir)
    };

    inode->flags = INODE_DIR;
    inode->mode = 0555;
    inode->name = namedup;
    inode->prv = p;
    *s->pr = s->r;
    free_search(s);
    return 0;

failure:
    free(p);
    free(namedup);
    return -1;
}

static int check_pvd(const struct iso9660_vd *const vd)
{
    const struct iso9660_pvd *const pvd = &vd->u.pvd;
    const struct iso9660_entry *const e = &pvd->root_dir;
    const struct iso9660_header *const h = &vd->header;
    const uint16_t block_size = iso9660_lmsb16(&pvd->block_size);
    const size_t exp_sz = sizeof (struct iso9660_entry) + sizeof pvd->padding;

    if (iso9660_check_header(&h->magic))
        return -1;
    else if (h->version != 1)
    {
        kprintf("Unsupported ISO9660 version %#hhx, expected 1\n", h->version);
        return -1;
    }
    else if (h->type != ISO9660_TYPE_PRIMARY_VD)
    {
        kprintf("Unexpected volume descriptor type %#hhx, expected=%#x\n",
            h->type, ISO9660_TYPE_PRIMARY_VD);
        return -1;
    }
    else if (pvd->fs_version != 1)
    {
        kprintf("Unexpected file structure version %#hhx, expected 1\n",
            pvd->fs_version);
        return -1;
    }
    else if (block_size != ISO9660_SECTOR_SZ)
    {
        kprintf("Unsupported block size %" PRIu16 ", expected %d\n",
            block_size, ISO9660_SECTOR_SZ);
        return -1;
    }
    else if (e->len != exp_sz)
    {
        kprintf("Unexpected root directory length %hhd, expected %zu\n",
            e->len, exp_sz);
        return -1;
    }
    else if (!(e->flags & ISO9660_FLAGS_DIR))
    {
        kprintf("Directory flag (%#hx) not found in file flags (%#hhx)\n",
            ISO9660_FLAGS_DIR, e->flags);
        return -1;
    }

    return 0;
}

static enum state read_pvd_done(void *const args)
{
    struct search *const s = args;
    struct fs_fd *const fd = &s->fd;
    const struct iso9660_vd *const vd = s->buf;
    const struct iso9660_pvd *const pvd = &vd->u.pvd;
    const struct iso9660_entry *const e = &pvd->root_dir;
    const uint32_t lba = iso9660_lmsb32(&e->extent_loc);

    fd->offset = lba * ISO9660_SECTOR_SZ;

    if (check_pvd(vd))
        goto failure;
    else if (!strcmp(s->src, "/"))
    {
        if (set_root_inode(pvd, s))
            goto failure;

        return STATE_AGAIN;
    }
    else if (next_token(s) || prepare_entry(s))
        goto failure;

    return STATE_AGAIN;

failure:
    free_search(s);
    return STATE_FATAL;
}

static enum state read_pvd(void *const args)
{
    struct search *const s = args;
    struct fs_fd *const fd = &s->fd;

    if (!(s->buf = malloc(ISO9660_SECTOR_SZ)) || next_token(s))
        goto failure;

    const struct fs_read r =
    {
        .buf = s->buf,
        .fd = fd,
        .n = ISO9660_SECTOR_SZ
    };

    *s->pr = (const struct fs_ret)
    {
        .f = read_pvd_done,
        .args = s
    };

    fd->offset = 16 * ISO9660_SECTOR_SZ;

    if (s->src_mp.fs->read(&r, s->pr))
        goto failure;

    return STATE_AGAIN;

failure:
    free_search(s);
    return STATE_FATAL;
}

static int src_done(const enum state state, const char *const relpath,
    const struct fs_mp *const mp, const union inode_result *const inode,
    void *const args)
{
    struct search *const s = args;

    if (state)
    {
        kprintf("Failed to find source inode for %s\n", s->src);
        return -1;
    }

    s->src_mp = *mp;
    *s->pr = (const struct fs_ret)
    {
        .f = read_pvd,
        .args = s
    };

    const struct fs_open o =
    {
        .fd = &s->fd,
        .path = mp->src,
        .flags = O_RDONLY
    };

    return mp->fs->open(&o, mp, inode, s->pr);
}

int iso9660_search(const char *const path, const struct fs_mp *const mp,
    union inode_result *const inode, struct fs_ret *const r)
{
    struct search *const s = malloc(sizeof *s);

    if (!s)
        goto failure;

    *s = (const struct search)
    {
        .inode = inode,
        .src = mp->src,
        .path = path,
        .r = *r,
        .pr = r
    };

    const struct inode_search is =
    {
        .path = mp->src,
        .done = src_done,
        .args = s,
    };

    if (inode_search(&is, r))
        goto failure;

    return 0;

failure:
    free(s);
    return -1;
}
