/*
 * 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 <drv/ps1/cd.h>
#include <drv/ps1/cd/routines.h>
#include <drv/ps1/cd/types.h>
#include <drv/ps1/event.h>
#include <drv/event.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>


#include <drv/ps1/bios.h>

static int seek(void);

static int deliver_event(struct cd_prv *const p, const int error)
{
    const struct cd_prv_read *const read = &p->u.read;
    const struct cd_req *const req = p->head;
    const struct drv_event_done *const d = &req->done;

    drv_ps1_event_disable(read->endevent);
    drv_ps1_event_close(read->endevent);
    drv_ps1_event_disable(read->errevent);
    drv_ps1_event_close(read->errevent);

    if (!error)
    {
        const struct cd_req_read *const rr = &req->u.read;
        const long offset = rr->offset % CD_SECTOR_SZ;

        memcpy(rr->buf, p->sector + offset, rr->n);

        if (!((p->offset += rr->n) % CD_SECTOR_SZ))
            p->sector_read = false;
    }

    if (d->f(error, d->args) || drv_ps1_cd_next())
        return -1;

    return 0;
}

static int wait_read(void)
{
    struct cd_prv *const p = &drv_ps1_cd_prv;
    const struct cd_prv_read *const read = &p->u.read;

    if (drv_ps1_event_test(p->event))
    {
        p->sector_read = true;
        return deliver_event(p, SUCCESS);
    }
    else if (drv_ps1_event_test(read->errevent)
        || drv_ps1_event_test(read->endevent))
        return deliver_event(p, EIO);

    return 0;
}

static int uncached_read(struct cd_prv *const p)
{
    int endevent, errevent;
    struct cd_prv_read *const r = &p->u.read;
    const struct CdAsyncReadSector_mode mode =
    {
        .mode.bits.speed = 1,
    };

    endevent = drv_ps1_event_open(CLASS_CDROM, SPEC_DATA_END, MODE_READY, NULL);
    errevent = drv_ps1_event_open(CLASS_CDROM, SPEC_ERROR, MODE_READY, NULL);

    if (endevent == -1 || errevent == -1)
        goto failure;

    *r = (const struct cd_prv_read)
    {
        .endevent = endevent,
        .errevent = errevent
    };

    drv_ps1_event_enable(endevent);
    drv_ps1_event_enable(errevent);

    Printf("preparing async read\n");
    if (!CdAsyncReadSector(1, p->sector, mode))
        goto failure;

    p->next = wait_read;
    return 0;

failure:

    if (endevent != -1)
    {
        drv_ps1_event_disable(endevent);
        drv_ps1_event_close(endevent);
    }

    if (errevent != -1)
    {
        drv_ps1_event_disable(errevent);
        drv_ps1_event_close(errevent);
    }

    return -1;
}

static int cached_read(struct cd_prv *const p)
{
    struct cd_req *const req = p->head;
    struct cd_req_read *const rr = &req->u.read;
    const struct drv_event_done *const done = &req->done;
    const off_t offset = rr->offset % CD_SECTOR_SZ,
        rem = CD_SECTOR_SZ - offset,
        n = rr->n > rem ? rem : rr->n;

    Printf("Doing %lu-byte cached read from offset %lu to %p\n",
            (unsigned long) n, (unsigned long) offset, (void *)rr->buf);
    memcpy(rr->buf, p->sector + offset, n);

    if (!((p->offset += n) % CD_SECTOR_SZ))
        p->sector_read = false;

    if (done->f(SUCCESS, done->args) || drv_ps1_cd_next())
        return -1;

    return 0;
}

static int wait_seek(void)
{
    struct cd_prv *const p = &drv_ps1_cd_prv;
    const struct cd_req *const req = p->head;
    const struct cd_req_read *const rr = &req->u.read;
    const struct cd_prv_read *const r = &p->u.read;

    if (drv_ps1_event_test(p->event))
    {
        Printf("finished seeking\n");
        drv_ps1_event_disable(r->errevent);
        drv_ps1_event_close(r->errevent);
        p->offset = rr->offset;
        return uncached_read(p);
    }
    else if (drv_ps1_event_test(r->errevent))
    {
        Printf("failed to seek\n");
        return deliver_event(p, EIO);
    }

    return 0;
}

static int seek(void)
{
    struct cd_prv *const p = &drv_ps1_cd_prv;
    struct cd_prv_read *const r = &p->u.read;
    const struct cd_req_read *rr = &p->head->u.read;
    const unsigned sector = rr->offset / CD_SECTOR_SZ;
    const struct CdAsyncSeekL seekl = drv_ps1_cd_toseekl(sector);
    const int errevent = drv_ps1_event_open(CLASS_CDROM, SPEC_ERROR,
        MODE_READY, NULL);

    if (errevent == -1)
        goto failure;

    *r = (const struct cd_prv_read){.errevent = errevent};
    p->sector_read = false;
    drv_ps1_event_enable(errevent);

    Printf("Seeking to offset %lu\n", (unsigned long)rr->offset);

    if (!CdAsyncSeekL(&seekl))
        goto failure;

    p->next = wait_seek;
    return 0;

failure:

    if (errevent != -1)
    {
        drv_ps1_event_disable(errevent);
        drv_ps1_event_close(errevent);
    }

    return -1;
}

static int start(void)
{
    struct cd_prv *const p = &drv_ps1_cd_prv;
    const struct cd_req *const req = p->head;
    const struct cd_req_read *const rr = &req->u.read;
    const off_t cursect = p->offset / CD_SECTOR_SZ,
        tgtsect = rr->offset / CD_SECTOR_SZ;

    if (cursect != tgtsect || !p->sector_read)
        return seek();

    /* TODO: multi-sector reads, make sure cache can really be used */
    return cached_read(p);
}

int drv_ps1_cd_read(void *const buf, const size_t n, const off_t offset,
    const struct drv_event_done *const done, void *const args)
{
    struct cd_prv *const p = &drv_ps1_cd_prv;
    struct cd_req *const r = malloc(sizeof *r);

    if (!r)
        return -1;

    *r = (const struct cd_req)
    {
        .f = start,
        .done = *done,
        .u.read =
        {
            .offset = offset,
            .buf = buf,
            .n = n
        }
    };

    if (!p->head)
        p->head = r;
    else
        p->tail->next = r;

    p->tail = r;
    Printf("Added %p {.buf=%p, .n=%lu} to read reqs\n", (void *)p->tail, buf,
        (unsigned long)n);
    return 0;
}
