/*
 * ch375sim.c - run the driver protocol (../ch375_hw.h) against a
 * simulated CH375 with an in-memory USB disk.
 *
 * The model follows what the DOS driver expects from the chip; it checks
 * the driver logic (packet loop, retries, recovery), not the real chip.
 *
 *   cc -O -Wall -o ch375sim ch375sim.c && ./ch375sim
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static unsigned int sim_inb(unsigned int port);
static void sim_outb(unsigned int val, unsigned int port);
static long sim_delay_ms;

#define CH_INB(port)            sim_inb(port)
#define CH_OUTB(val, port)      sim_outb((val) & 0xFF, port)
#define CH_MDELAY(ms)           (sim_delay_ms += (ms))
#define CH_LOG(args)            printf args
#include "../ch375_hw.h"

#define BASE            0x260
#define DISK_SECTORS    4096

static struct {
        int present;
        unsigned char ver;
        int attached;
        unsigned char vars[256];
        unsigned char cmd;
        int argn;
        unsigned char args[8];
        unsigned char out[CH_CHUNK + 1];
        int outn, outpos;
        int intpend;
        unsigned char status;
        unsigned char usbbuf[CH_CHUNK];
        int usblen;
        /* current DISK_READ / DISK_WRITE */
        int xfer;               /* 0 none, 'r' or 'w' */
        unsigned long lba;
        unsigned int chunks, done;
        unsigned char wbuf[CH_CHUNK];
        int wlen;
        /* fault injection: count interrupts raised */
        long nraise;
        long drop_at;           /* this interrupt never comes (timeout) */
        long err_at;            /* this interrupt reports a USB error */
        long reads, writes;
} sim;

static unsigned char disk[DISK_SECTORS * 512];
static unsigned char ref[DISK_SECTORS * 512];

static void sim_reply(const unsigned char *p, int n)
{
        memcpy(sim.out, p, n);
        sim.outn = n;
        sim.outpos = 0;
}

static void sim_reply1(unsigned char v)
{
        sim_reply(&v, 1);
}

static void sim_raise(unsigned char st)
{
        sim.nraise++;
        if (sim.nraise == sim.drop_at)
                return;
        if (sim.nraise == sim.err_at)
                st = 0x1F;              /* USB_INT_DISK_ERR */
        sim.status = st;
        sim.intpend = 1;
}

static void sim_load_chunk(void)
{
        memcpy(sim.usbbuf, disk + sim.lba * 512 + sim.done * CH_CHUNK, CH_CHUNK);
        sim.usblen = CH_CHUNK;
}

static void sim_command(unsigned char cmd)
{
        unsigned char cap[8];
        unsigned long last;

        sim.cmd = cmd;
        sim.argn = 0;
        sim.outn = sim.outpos = 0;
        switch (cmd) {
        case CH_CMD_GET_IC_VER:
                sim_reply1(sim.ver);
                break;
        case CH_CMD_GET_STATUS:
                sim_reply1(sim.status);
                sim.intpend = 0;
                break;
        case CH_CMD_RD_USB_DATA:
                sim.out[0] = sim.usblen;
                memcpy(sim.out + 1, sim.usbbuf, sim.usblen);
                sim.outn = sim.usblen + 1;
                break;
        case CH_CMD_WR_USB_DATA7:
                sim.wlen = -1;
                break;
        case CH_CMD_ABORT_NAK:
                sim.xfer = 0;
                break;
        case CH_CMD_DISK_INIT:
                if (sim.attached) {
                        sim.vars[CH_VAR_FLAGS] |= 0x01;
                        sim_raise(CH_INT_SUCCESS);
                } else
                        sim_raise(0x16);        /* disconnect */
                break;
        case CH_CMD_DISK_SIZE:
                last = DISK_SECTORS - 1;
                cap[0] = last >> 24; cap[1] = last >> 16; cap[2] = last >> 8; cap[3] = last;
                cap[4] = 0; cap[5] = 0; cap[6] = 2; cap[7] = 0;
                memcpy(sim.usbbuf, cap, 8);
                sim.usblen = 8;
                sim_raise(CH_INT_SUCCESS);
                break;
        case CH_CMD_DISK_RESET:
                sim.xfer = 0;
                /* fall through */
        case CH_CMD_DISK_READY:
        case CH_CMD_DISK_R_SENSE:
                sim_raise(CH_INT_SUCCESS);
                break;
        case CH_CMD_DISK_RD_GO:
                if (sim.xfer != 'r') {
                        printf("sim: RD_GO without a read\n");
                        exit(1);
                }
                if (++sim.done == sim.chunks) {
                        sim.xfer = 0;
                        sim_raise(CH_INT_SUCCESS);
                } else {
                        sim_load_chunk();
                        sim_raise(CH_INT_DISK_READ);
                }
                break;
        case CH_CMD_DISK_WR_GO:
                if (sim.xfer != 'w' || sim.wlen != CH_CHUNK) {
                        printf("sim: bad WR_GO (xfer %d len %d)\n", sim.xfer, sim.wlen);
                        exit(1);
                }
                memcpy(disk + sim.lba * 512 + sim.done * CH_CHUNK, sim.wbuf, CH_CHUNK);
                if (++sim.done == sim.chunks) {
                        sim.xfer = 0;
                        sim_raise(CH_INT_SUCCESS);
                } else
                        sim_raise(CH_INT_DISK_WRITE);
                break;
        }
}

static void sim_data(unsigned char v)
{
        unsigned char *a = sim.args;

        if (sim.cmd == CH_CMD_WR_USB_DATA7) {
                if (sim.wlen < 0)
                        sim.wlen = 0;   /* length byte */
                else if (sim.wlen < CH_CHUNK)
                        sim.wbuf[sim.wlen++] = v;
                return;
        }
        if (sim.argn < (int) sizeof sim.args)
                a[sim.argn++] = v;
        switch (sim.cmd) {
        case CH_CMD_CHECK_EXIST:
                sim_reply1(~v);
                break;
        case CH_CMD_READ_VAR8:
                sim_reply1(sim.vars[v]);
                break;
        case CH_CMD_WRITE_VAR8:
                if (sim.argn == 2)
                        sim.vars[a[0]] = a[1];
                break;
        case CH_CMD_SET_USB_MODE:
                sim_reply1(0x51);       /* CMD_RET_SUCCESS */
                break;
        case CH_CMD_SET_TOGGLE:
                sim_raise(CH_INT_SUCCESS);
                break;
        case CH_CMD_DISK_READ:
        case CH_CMD_DISK_WRITE:
                if (sim.argn != 5)
                        break;
                sim.lba = a[0] | a[1] << 8 | (unsigned long) a[2] << 16 | (unsigned long) a[3] << 24;
                sim.chunks = a[4] * (512 / CH_CHUNK);
                sim.done = 0;
                if (a[4] == 0 || sim.lba + a[4] > DISK_SECTORS) {
                        sim_raise(0x1F);
                        break;
                }
                if (sim.cmd == CH_CMD_DISK_READ) {
                        sim.xfer = 'r';
                        sim.reads++;
                        sim_load_chunk();
                        sim_raise(CH_INT_DISK_READ);
                } else {
                        sim.xfer = 'w';
                        sim.writes++;
                        sim_raise(CH_INT_DISK_WRITE);
                }
                break;
        }
}

static unsigned int sim_inb(unsigned int port)
{
        if (port == 0x61)
                return 0;
        if (!sim.present)
                return 0xFF;
        if (port == BASE + 1)
                return sim.intpend ? 0x00 : 0x80;
        if (port == BASE) {
                if (sim.outpos < sim.outn)
                        return sim.out[sim.outpos++];
                /* older chips: INT# shows on the idle data port */
                return sim.intpend ? 0x00 : 0x80;
        }
        printf("sim: inb from 0x%x\n", port);
        exit(1);
}

static void sim_outb(unsigned int val, unsigned int port)
{
        if (!sim.present)
                return;
        if (port == BASE + 1)
                sim_command(val);
        else if (port == BASE)
                sim_data(val);
        else {
                printf("sim: outb 0x%x to 0x%x\n", val, port);
                exit(1);
        }
}

static void sim_reset(int present, unsigned char ver)
{
        memset(&sim, 0, sizeof sim);
        sim.present = present;
        sim.ver = ver;
        sim.attached = 1;
        sim.vars[CH_VAR_STATE] = CH_STATE_ATTACHED;
        sim.drop_at = sim.err_at = -1;
}

static int failures;

static void check(int cond, const char *what)
{
        printf("%-58s %s\n", what, cond ? "ok" : "FAILED");
        if (!cond)
                failures++;
}

/* random reads and writes, checked against a reference copy */
static int exercise(struct ch375 *c, int rounds)
{
        static unsigned char buf[CH_MAX_SECTORS * 512];
        unsigned long lba;
        unsigned int n, i;
        int r;

        for (r = 0; r < rounds; r++) {
                n = 1 + rand() % CH_MAX_SECTORS;
                lba = rand() % (DISK_SECTORS - n + 1);
                if (rand() & 1) {
                        for (i = 0; i < n * 512; i++)
                                buf[i] = rand();
                        if (!ch_xfer(c, 1, lba, n, buf))
                                return 0;
                        memcpy(ref + lba * 512, buf, n * 512);
                } else {
                        if (!ch_xfer(c, 0, lba, n, buf))
                                return 0;
                        if (memcmp(buf, ref + lba * 512, n * 512))
                                return 0;
                }
        }
        return memcmp(disk, ref, sizeof disk) == 0;
}

static void init_disk(void)
{
        unsigned int i;

        for (i = 0; i < sizeof disk; i++)
                disk[i] = i * 7 + (i >> 9);
        memcpy(ref, disk, sizeof disk);
}

static int bring_up(struct ch375 *c, unsigned char ver, int expect_mode)
{
        memset(c, 0, sizeof *c);
        c->base = BASE;
        sim_reset(1, ver);
        if (!ch_check_exist(c))
                return 0;
        if (ch_reset_init(c) != 0x51 || c->version != ver || c->mode != expect_mode)
                return 0;
        return ch_mount(c) && c->sectors == DISK_SECTORS && c->blksize == 512;
}

int main(void)
{
        static unsigned char buf[4 * 512];
        struct ch375 c;
        long before;

        srand(375);
        init_disk();

        memset(&c, 0, sizeof c);
        c.base = BASE;
        sim_reset(0, 0);
        check(!ch_check_exist(&c), "no chip: CHECK_EXIST fails");

        check(bring_up(&c, 0xB7, 2), "CH375B (0xB7): init, mode 2, mount, 4096 sectors");
        check(exercise(&c, 300), "300 random reads/writes of 1..255 sectors");
        check(sim_delay_ms == 2, "fast path does no millisecond waits");

        before = c.timeouts;
        sim.drop_at = sim.nraise + 5;           /* lose an interrupt mid-transfer */
        check(ch_xfer(&c, 0, 100, 4, buf) && !memcmp(buf, ref + 100 * 512, 512),
              "lost interrupt during read: recovered and retried");
        check(c.timeouts == before + 1, "  timeout counted");

        sim.drop_at = sim.nraise + 3;
        memset(buf, 0x5A, sizeof buf);
        check(ch_xfer(&c, 1, 7, 1, buf), "lost interrupt during write: recovered and retried");
        memcpy(ref + 7 * 512, buf, 512);

        sim.err_at = sim.nraise + 2;
        check(ch_xfer(&c, 0, 7, 1, buf) && buf[0] == 0x5A, "USB error status during read: retried");
        check(exercise(&c, 50), "disk still consistent after faults");

        sim.vars[CH_VAR_FLAGS] = 0x80;          /* chip forgot the disk */
        check(ch_xfer(&c, 0, 0, 1, buf) && !memcmp(buf, ref, 512), "remount when the disk is no longer mounted");

        sim.attached = 0;
        sim.vars[CH_VAR_STATE] = 0;
        sim.vars[CH_VAR_FLAGS] = 0x80;
        check(!ch_xfer(&c, 0, 0, 1, buf), "unplugged disk: transfer fails");

        check(bring_up(&c, 0xA8, 1), "CH375A (0xA8): init, mode 1, mount");
        check(exercise(&c, 30), "mode 1 random reads/writes");
        check(bring_up(&c, 0x9A, 0), "old chip (0x9A): init, mode 0, mount");
        check(sim.vars[CH_VAR_OLDCHIP] == 0x8F, "  old-chip variable set");
        check(exercise(&c, 30), "mode 0 random reads/writes");

        printf("%s\n", failures ? "SOME TESTS FAILED" : "all tests passed");
        return failures != 0;
}

