/* 3b2_d.c: uPD7261 Integrated Disk Controller

   Copyright (c) 2017-2022, Seth J. Morabito

   Permission is hereby granted, free of charge, to any person
   obtaining a copy of this software and associated documentation
   files (the "Software"), to deal in the Software without
   restriction, including without limitation the rights to use, copy,
   modify, merge, publish, distribute, sublicense, and/or sell copies
   of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be
   included in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.

   Except as contained in this notice, the name of the author shall
   not be used in advertising or otherwise to promote the sale, use or
   other dealings in this Software without prior written authorization
   from the author.
*/

/*
 * This file contains the code for the Integrated Disk (ID) controller
 * (based on the uPD7261) and up to two winchester hard disks.
 *
 * Supported winchester drives are:
 *
 *   SIMH Name  ID    Cyl  Head  Sec  Byte/Sec  Note
 *   ---------  --   ----  ----  ---  --------  ----------------------
 *   HD30       3     697     5   18    512     CDC Wren 94155-36
 *   HD72       5     925     9   18    512     CDC Wren II 94156-86
 *   HD72C      8     754    11   18    512     Fujitsu M2243AS
 *   HD135      11   1224    15   18    512     Maxtor XT1190
 */

#include "3b2_id.h"

#include "sim_disk.h"

#include "3b2_cpu.h"

#define ID_STEP_WAIT        4000   /* Delay per track step */
#define ID_STEP_MIN         10000  /* Minimum delay for RECAL/SEEK command */
#define ID_RW_WAIT          5000
#define ID_SUS_WAIT         1000
#define ID_SPEC_WAIT        1000
#define ID_SIS_WAIT         1000
#define ID_CMD_WAIT         2000

#define POLLING(id)         ((id_dtlh & ID_DTLH_POLL) == 0)

/* Static function declarations */
static SIM_INLINE t_lba id_lba(uint16 cyl, uint8 head, uint8 sec);

/* DMAC request */
t_bool id_drq = FALSE;

struct id_state id_state[ID_NUM_UNITS] = {0};

/* Enable support for disks with > 1024 cylinders */
static t_bool   id_large = FALSE;
/* Data FIFO pointer - Read */
static uint8    id_dpr = 0;
/* Data FIFO pointer - Write */
static uint8    id_dpw = 0;
/* Controller Status Register */
static uint8    id_status = 0;
/* Unit Interrupt Status */
static uint8    id_int_status = 0;
/* Last command received */
static uint8    id_cmd = 0;
/* 8-byte FIFO */
static uint8    id_data[ID_FIFO_LEN] = {0};
/* SRQM bit */
static t_bool   id_srqm = FALSE;
/* The logical unit number (0-1) */
static uint8    id_unit_num = 0;
/* The physical unit number (0-3) */
static uint8    id_ua = 0;
/* Whether we are using buffered SEEK/RECAL or not */
static t_bool   id_buffered = FALSE;
/* Sector buffer */
static uint8    id_buf[ID_SEC_SIZE];
/* Buffer pointer */
static size_t   id_buf_ptr = 0;

/* SPECIFY parameters */
static uint8    id_esn;
static uint8    id_etn;
static uint8    id_dtlh;

static uint8    id_idfield[ID_IDFIELD_LEN];
static uint8    id_idfield_ptr = 0;

struct id_dtype {
    uint8  hd;    /* Number of heads */
    uint32 capac; /* Capacity (in sectors) */
    const char *name;
};

static struct id_dtype id_dtab[] = {
    ID_DRV(HD30),
    ID_DRV(HD72),
    ID_DRV(HD72C),
    ID_DRV(HD135),
    ID_DRV(HD161),   /* Only enabled if 'id_large' is set */
    { 0 }
};

UNIT id_unit[] = {
    { UDATA (&id_unit_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_BINK+DKUF_NOAUTOSIZE+
             (ID_HD72_DTYPE << ID_V_DTYPE), ID_DSK_SIZE(HD72)), 0, ID0, 0 },
    { UDATA (&id_unit_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_BINK+DKUF_NOAUTOSIZE+
             (ID_HD72_DTYPE << ID_V_DTYPE), ID_DSK_SIZE(HD72)), 0, ID1, 0 },
    { UDATA (&id_ctlr_svc, 0, 0) },
    { NULL }
};

UNIT    *id_ctlr_unit = &id_unit[ID_CTLR];

/* The currently selected drive number */
UNIT    *id_sel_unit = &id_unit[ID0];

REG id_reg[] = {
    { HRDATAD(CMD,  id_cmd,    8, "Command") },
    { HRDATAD(STAT, id_status, 8, "Status")  },
    { NULL }
};

MTAB id_mod[] = {
    { MTAB_XTD|MTAB_VDV, 1, NULL, "LARGE",
      &id_set_large, NULL, NULL, "Enable support for disks with > 1024 cylinders" },
    { MTAB_XTD|MTAB_VDV, 0, NULL, "NOLARGE",
      &id_set_large, NULL, NULL, "Disable support disks with > 1024 cylinders" },
    { MTAB_XTD|MTAB_VUN, ID_HD30_DTYPE, NULL, "HD30",
      &id_set_type, NULL, NULL, "Set HD30 Disk Type" },
    { MTAB_XTD|MTAB_VUN, ID_HD72_DTYPE, NULL, "HD72",
      &id_set_type, NULL, NULL, "Set HD72 Disk Type" },
    { MTAB_XTD|MTAB_VUN, ID_HD72C_DTYPE, NULL, "HD72C",
      &id_set_type, NULL, NULL, "Set HD72C Disk Type" },
    { MTAB_XTD|MTAB_VUN, ID_HD135_DTYPE, NULL, "HD135",
      &id_set_type, NULL, NULL, "Set HD135 Disk Type" },
    { MTAB_XTD|MTAB_VUN, ID_HD161_DTYPE, NULL, "HD161",
      &id_set_type, NULL, NULL, "Set HD161 Disk Type" },
    { MTAB_XTD|MTAB_VUN, 0, "TYPE", NULL,
      NULL, &id_show_type, NULL, "Display device type" },
    { 0 }
};

DEVICE id_dev = {
    "IDISK", id_unit, id_reg, id_mod,
    ID_NUM_UNITS, 16, 32, 1, 16, 8,
    NULL, NULL, &id_reset,
    NULL, &id_attach, &id_detach, NULL,
    DEV_DEBUG|DEV_DISK|DEV_SECTORS, 0, sys_deb_tab,
    NULL, NULL, &id_help, NULL, NULL,
    &id_description
};

#define UPDATE_INT {                                     \
        if ((id_status & ID_IRQ_MASK) && !id_srqm) {     \
            CPU_SET_INT(INT_DISK);                       \
        } else {                                         \
            CPU_CLR_INT(INT_DISK);                       \
        }                                                \
    }

static SIM_INLINE void id_set_status(uint8 flags)
{
    id_status |= flags;
    UPDATE_INT;
}

static SIM_INLINE void id_clr_status(uint8 flags)
{
    id_status &= ~(flags);
    UPDATE_INT;
}

static SIM_INLINE void id_set_srqm(t_bool state)
{
    id_srqm = state;
    UPDATE_INT;
}

static SIM_INLINE void id_clear_fifo()
{
    id_dpr = 0;
    id_dpw = 0;
}

static SIM_INLINE void id_activate(UNIT *uptr, int32 delay)
{
    sim_activate_abs(uptr, delay);
}

/*
 * Service routine for ID controller.
 *
 * The simulated HD controller must service Sense Interrupt Status,
 * Specify, and Detect Error independent of the operation of either ID
 * unit, which may be in the middle of a seek or other operation.
 */
t_stat id_ctlr_svc(UNIT *uptr)
{
    uint8 cmd;

    cmd = uptr->u4;  /* The command that caused the activity */

    id_set_srqm(FALSE);
    id_clr_status(ID_STAT_CB);
    id_set_status(ID_STAT_CEH);
    uptr->u4 = 0;

    switch (cmd) {
    case ID_CMD_SIS:
        sim_debug(EXECUTE_MSG, &id_dev,
                  "INTR\t\tCOMPLETING Sense Interrupt Status.\n");
        id_data[0] = id_int_status;
        id_int_status = 0;
        break;
    default:
        sim_debug(EXECUTE_MSG, &id_dev,
                  "INTR\t\tCOMPLETING OTHER COMMAND 0x%x (CONTROLLER)\n",
                  cmd);
        break;
    }

    return SCPE_OK;
}

/*
 * Service routine for ID0 and ID1 units.
 */
t_stat id_unit_svc(UNIT *uptr)
{
    t_bool recal_error;
    uint8 unit, other, cmd, end_flags;

    unit  = uptr->u3;  /* The unit number that needs an interrupt */
    cmd   = uptr->u4;  /* The command that caused the activity    */
    other = unit ^ 1;  /* The number of the other unit            */

    recal_error = FALSE;

    /* If the other unit is active, we cannot interrupt, so we delay
     * here */
    if (id_unit[other].u4 == ID_CMD_RDATA ||
        id_unit[other].u4 == ID_CMD_WDATA) {
        id_activate(uptr, 1000);
        return SCPE_OK;
    }

    id_set_srqm(FALSE);

    switch (cmd) {
    case ID_CMD_RECAL:
        /* Recalibrate ends in an error condition if we were never able to
           reach track 0 */
        recal_error = (id_state[unit].cyl != 0);
        /* intentional fall-through */
    case ID_CMD_SEEK:
        id_int_status = 0;
        uptr->u4 = 0;

        sim_debug(EXECUTE_MSG, &id_dev,
                  "INTR\t\tCOMPLETING Recal/Seek UNIT %d (polling=%d)\n",
                  unit, POLLING(unit));

        if (recal_error) {
            end_flags = 0;
        } else {
            end_flags = POLLING(unit) ? ID_STAT_SRQ : ID_STAT_CEH;
        }

        if (uptr->flags & UNIT_ATT) {
            id_set_status(end_flags);
            id_int_status |= (ID_IST_SEN|unit);
        } else {
            id_int_status |= (ID_IST_NR|unit);
        }

        id_data[0] = id_int_status;
        break;
    case ID_CMD_SUS:
        sim_debug(EXECUTE_MSG, &id_dev,
                  "INTR\t\tCOMPLETING Sense Unit Status UNIT %d\n",
                  unit);
        id_set_status(ID_STAT_CEH);
        uptr->u4 = 0;
        if ((uptr->flags & UNIT_ATT) == 0) {
            /* If no HD is attached, SUS puts 0x00 into the data
               buffer */
            id_data[0] = 0;
        } else {
            /* Put Unit Status into byte 0 */
            id_data[0] = (ID_UST_DSEL|ID_UST_SCL|ID_UST_RDY);
            if (id_state[unit].cyl == 0) {
                id_data[0] |= ID_UST_TK0;
            }
        }
        break;
    default:
        sim_debug(EXECUTE_MSG, &id_dev,
                  "INTR\t\tCOMPLETING OTHER COMMAND 0x%x UNIT %d\n",
                  cmd, unit);
        id_set_status(ID_STAT_CEH);
        uptr->u4 = 0;
        break;
    }

    /* In all conditions except on a RECALIBRATE failure, CB is cleared */
    if (!recal_error) {
        id_clr_status(ID_STAT_CB);
    }

    return SCPE_OK;
}

t_stat id_set_large(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
    id_large = (t_bool)val;

    if (!id_large && (ID_GET_DTYPE(id_unit[0].flags) > ID_HD135_DTYPE ||
                      ID_GET_DTYPE(id_unit[1].flags) > ID_HD135_DTYPE)) {
        return sim_messagef(SCPE_OK, "Large disk support disabled, but at least one attached disk is large!\n");
    }

    return SCPE_OK;
}

t_stat id_set_type(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
    if (val < 0 || val > ID_MAX_DTYPE) {
        return SCPE_ARG;
    }

    if (uptr->flags & UNIT_ATT) {
        return SCPE_ALATT;
    }

    uptr->flags = (uptr->flags & ~ID_DTYPE) | (val << ID_V_DTYPE);
    uptr->capac = (t_addr)id_dtab[val].capac;

    /*
     * Warn the user after setting the type, so we can guard against accidentally
     * attaching to a disk we can't use.
     */
    if (val == ID_HD161_DTYPE && !id_large) {
        return sim_messagef(SCPE_OK, "HD161 disks can only be used if large disk support is enabled (SET IDISK LARGE)\n");
    }

    return SCPE_OK;
}

t_stat id_show_type (FILE *st, UNIT *uptr, int32 val, CONST void *desc)
{
    fprintf(st, "%s%s",
            id_dtab[ID_GET_DTYPE(uptr->flags)].name,
            id_large ? " (Large Disk Support Enabled)" : "");
    return SCPE_OK;
}

t_stat id_reset(DEVICE *dptr)
{
    id_clear_fifo();
    return SCPE_OK;
}

t_stat id_attach(UNIT *uptr, CONST char *cptr)
{

    if (ID_GET_DTYPE(uptr->flags) == ID_HD161_DTYPE && !id_large) {
        return sim_messagef(SCPE_ARG, "HD161 disks can only be used if large disk support is enabled (SET IDISK LARGE)\n");
    }

    return sim_disk_attach_ex(uptr, cptr, 512, 1, TRUE, 0, id_dtab[ID_GET_DTYPE(uptr->flags)].name, 0, 0, NULL);
}

t_stat id_detach(UNIT *uptr)
{
    return sim_disk_detach(uptr);
}

/* Return the logical block address of the given sector */
static t_lba id_lba(uint16 cyl, uint8 head, uint8 sec)
{
    uint8 dtype;

    dtype = ID_GET_DTYPE(id_sel_unit->flags);

    return((ID_SEC_CNT * id_dtab[dtype].hd * cyl) +
           (ID_SEC_CNT * head) +
           sec);
}

/* At the end of each sector read or write, we update the FIFO
 * with the correct return parameters. */
static void SIM_INLINE id_end_rw(uint8 est)
{
    uint8 id = id_sel_unit->u3;

    id_clear_fifo();
    id_data[0] = est;
    id_data[1] = id_state[id].phn;
    id_data[2] = ~(id_state[id].lcnh);
    id_data[3] = id_state[id].lcnl;
    id_data[4] = id_state[id].lhn;
    id_data[5] = id_state[id].lsn;
    id_data[6] = id_state[id].scnt;
}

/* The controller wraps id_lsn, id_lhn, and id_lcnl on each sector
 * read, so that they point to the next C/H/S */
static void SIM_INLINE id_update_chs()
{
    uint8 id = id_sel_unit->u3;

    if (id_state[id].lsn++ >= id_esn) {
        id_state[id].lsn = 0;
        if (id_state[id].lhn++ >= id_etn) {
            id_state[id].lhn = 0;
            if (id_state[id].lcnl == 0xff) {
                id_state[id].lcnl = 0;
                id_state[id].lcnh++;
            } else {
                id_state[id].lcnl++;
            }
        }
    }
}

uint32 id_read(uint32 pa, size_t size)
{
    uint8 reg, id;
    uint16 cyl;
    t_lba lba;
    uint32 data;
    t_seccnt sectsread;

    id = id_sel_unit->u3;
    reg = (uint8) (pa - IDBASE);

    switch(reg) {
    case ID_DATA_REG:     /* Data Buffer Register */
        /* If we're in a DMA transfer, we need to be reading data from
         * the disk buffer. Otherwise, we're reading from the FIFO. */

        if (id_drq) {
            /* If the drive isn't attached, there's really nothing we
               can do. */
            if ((id_sel_unit->flags & UNIT_ATT) == 0) {
                id_end_rw(ID_EST_NR);
                return 0;
            }

            /* We could be in one of these commands:
             *    - Read Data
             *    - Read ID
             */

            if (CMD_NUM == ID_CMD_RDATA) {
                /* If we're still in DRQ but we've read all our sectors,
                 * that's an error state. */
                if (id_state[id].scnt == 0) {
                    sim_debug(READ_MSG, &id_dev,
                              "ERROR\tid_scnt = 0 but still in dma\n");
                    id_end_rw(ID_EST_OVR);
                    return 0;
                }

                /* If the disk buffer is empty, fill it. */
                if (id_buf_ptr == 0 || id_buf_ptr >= ID_SEC_SIZE) {
                    /* It's time to read a new sector into our sector buf */
                    id_buf_ptr = 0;
                    cyl = (uint16) (((uint16)id_state[id].lcnh << 8)|(uint16)id_state[id].lcnl);
                    id_state[id_unit_num].cyl = cyl;
                    lba = id_lba(cyl, id_state[id].lhn, id_state[id].lsn);
                    if (sim_disk_rdsect(id_sel_unit, lba, id_buf, &sectsread, 1) == SCPE_OK) {
                        if (sectsread !=1) {
                            sim_debug(READ_MSG, &id_dev,
                                      "ERROR: ASKED TO READ ONE SECTOR, READ: %d\n",
                                      sectsread);
                        }
                        id_update_chs();
                    } else {
                        /* Uh-oh! */
                        sim_debug(READ_MSG, &id_dev,
                                  "RDATA READ ERROR. Failure from sim_disk_rdsect!\n");
                        id_end_rw(ID_EST_DER);
                        return 0;
                    }
                }

                data = id_buf[id_buf_ptr++];
                sim_debug(READ_MSG, &id_dev, "DATA\t%02x\n", data);

                /* Done with this current sector, update id_scnt */
                if (id_buf_ptr >= ID_SEC_SIZE) {
                    if (--id_state[id].scnt == 0) {
                        id_end_rw(0);
                    }
                }
            } else if (CMD_NUM == ID_CMD_RID) {
                /* We have to return the ID bytes for the current C/H/S */
                if (id_idfield_ptr == 0 || id_idfield_ptr >= ID_IDFIELD_LEN) {
                    id_idfield[0] = ~(id_state[id].lcnh);
                    id_idfield[1] = id_state[id].lcnl;
                    id_idfield[2] = id_state[id].lhn;
                    id_idfield[3] = id_state[id].lsn;
                    id_idfield_ptr = 0;
                }

                data = id_idfield[id_idfield_ptr++];
                sim_debug(READ_MSG, &id_dev,
                          "ID DATA\t%02x\n",
                          data);

                if (id_idfield_ptr >= ID_IDFIELD_LEN) {
                    if (id_state[id].scnt-- > 0) {
                        /* Another sector to ID */
                        id_idfield_ptr = 0;
                    } else {
                        /* All done, set return codes */
                        id_clear_fifo();
                        id_data[0] = 0;
                        id_data[1] = id_state[id].scnt;
                    }
                }
            } else {
                /* cmd not Read Data or Read ID */
                stop_reason = STOP_ERR;
                return 0;
            }

            return data;
        } else {
            if (id_dpr < ID_FIFO_LEN) {
                sim_debug(READ_MSG, &id_dev,
                          "DATA\t%02x\n",
                          id_data[id_dpr]);
                return id_data[id_dpr++];
            } else {
                sim_debug(READ_MSG, &id_dev,
                          "ERROR\tFIFO OVERRUN\n");
                return 0;
            }
        }

        break;
    case ID_CMD_STAT_REG:     /* Status Register */
        sim_debug(READ_MSG, &id_dev,
                  "STATUS\t%02x\n",
                  id_status|id_drq);
        return id_status|(id_drq ? 1u : 0);
    }

    sim_debug(READ_MSG, &id_dev,
              "Read of unsuported register %x\n",
              id_status);

    return 0;
}

void id_write(uint32 pa, uint32 val, size_t size)
{
    uint8 reg, id;
    uint16 cyl;
    t_lba lba;
    t_seccnt sectswritten;

    reg = (uint8) (pa - IDBASE);
    id = id_sel_unit->u3;

    switch(reg) {
    case ID_DATA_REG:
        /* If we're in a DMA transfer, we need to be writing data to
         * the disk buffer. Otherwise, we're writing to the FIFO. */

        if (id_drq) {
            /* If we're still in DRQ but we've written all our sectors,
             * that's an error state. */
            if (id_state[id].scnt == 0) {
                sim_debug(WRITE_MSG, &id_dev,
                          "ERROR\tid_scnt = 0 but still in dma\n");
                id_end_rw(ID_EST_OVR);
                return;
            }

            /* Write to the disk buffer */
            if (id_buf_ptr < ID_SEC_SIZE) {
                id_buf[id_buf_ptr++] = (uint8)(val & 0xff);
                sim_debug(WRITE_MSG, &id_dev,
                          "DATA\t%02x\n",
                          (uint8)(val & 0xff));
            } else {
                sim_debug(WRITE_MSG, &id_dev,
                          "ERROR\tWDATA OVERRUN\n");
                id_end_rw(ID_EST_OVR);
                return;
            }

            /* If we've hit the end of a sector, flush it */
            if (id_buf_ptr >= ID_SEC_SIZE) {
                /* It's time to start the next sector, and flush the old. */
                id_buf_ptr = 0;
                cyl = (uint16) (((uint16) id_state[id].lcnh << 8)|(uint16)id_state[id].lcnl);
                id_state[id].cyl = cyl;
                lba = id_lba(cyl, id_state[id].lhn, id_state[id].lsn);
                if (sim_disk_wrsect(id_sel_unit, lba, id_buf, &sectswritten, 1) == SCPE_OK) {
                    if (sectswritten !=1) {
                        sim_debug(WRITE_MSG, &id_dev,
                                  "ERROR: ASKED TO WRITE ONE SECTOR, WROTE: %d\n",
                                  sectswritten);
                    }
                    id_update_chs();
                    if (--id_state[id].scnt == 0) {
                        id_end_rw(0);
                    }
                } else {
                    /* Uh-oh! */
                    sim_debug(WRITE_MSG, &id_dev,
                              "ERROR\tWDATA WRITE ERROR. lba=%04x\n",
                              lba);
                    id_end_rw(ID_EST_DER);
                    return;
                }
            }
            return;
        } else {
            sim_debug(WRITE_MSG, &id_dev,
                      "DATA\t%02x\n",
                      val);
            if (id_dpw < ID_FIFO_LEN) {
                id_data[id_dpw++] = (uint8) val;
            } else {
                sim_debug(WRITE_MSG, &id_dev,
                          "ERROR\tFIFO OVERRUN\n");
            }
        }
        return;
    case ID_CMD_STAT_REG:
        id_handle_command((uint8) val);
        return;
    default:
        return;
    }
}

void id_handle_command(uint8 val)
{
    uint8 cmd, aux_cmd, sec, pattern, id;
    uint16 cyl;
    uint32 time;
    t_lba lba;

    /* Reset the FIFO pointer */
    id_clear_fifo();

    /* Is this an aux command or a full command? */
    if ((val & 0xf0) == 0) {
        aux_cmd = val & 0x0f;

        if (aux_cmd & ID_AUX_CLCE) {
            sim_debug(WRITE_MSG, &id_dev,
                      "COMMAND\t%02x\tAUX:CLCE\n",
                      val);
            id_clr_status(ID_STAT_CEH|ID_STAT_CEL);
        }

        if (aux_cmd & ID_AUX_HSRQ) {
            sim_debug(WRITE_MSG, &id_dev,
                      "COMMAND\t%02x\tAUX:HSRQ\n",
                      val);
            id_set_srqm(TRUE);
        }

        if (aux_cmd & ID_AUX_CLB) {
            sim_debug(WRITE_MSG, &id_dev,
                      "COMMAND\t%02x\tAUX:CLBUF\n",
                      val);
            id_clear_fifo();
        }

        if (aux_cmd & ID_AUX_RST) {
            sim_debug(WRITE_MSG, &id_dev,
                      "COMMAND\t%02x\tAUX:RESET\n",
                      val);
            id_clear_fifo();
            sim_cancel(id_sel_unit);
            sim_cancel(id_ctlr_unit);
            id_status = 0;
            id_set_srqm(FALSE);
        }

        /* Just return early */
        return;
    }

    /* If the controller is busy and this isn't an AUX command, do
     * nothing */
    if (id_status & ID_STAT_CB) {
        sim_debug(EXECUTE_MSG, &id_dev,
                  "Controller Busy. Skipping command byte %02x\n",
                  val);
        return;
    }

    /* A full command always resets CEH and CEL */
    id_clr_status(ID_STAT_CEH|ID_STAT_CEL);

    /* Save the full command byte */
    id_cmd = val;
    cmd = (id_cmd >> 4) & 0xf;

    if ((id_cmd & 3) != id_ua) {
        id_unit_num = id_cmd & 1;
        id_ua = id_cmd & 3;
        id_sel_unit = &id_unit[id_unit_num];
    }

    id = id_unit_num;

    /* TODO: Fix this hack */
    if (cmd == ID_CMD_SIS || cmd == ID_CMD_SPEC || cmd == ID_CMD_DERR) {
        id_ctlr_unit->u4 = cmd;
    } else {
        id_sel_unit->u4 = cmd;
    }

    id_set_status(ID_STAT_CB);

    switch(cmd) {
    case ID_CMD_SIS:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tSense Int. Status\n",
                  val);
        id_clr_status(ID_STAT_SRQ); /* SIS immediately de-asserts SRQ */
        id_activate(id_ctlr_unit, ID_SIS_WAIT);
        break;
    case ID_CMD_SPEC:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tSpecify - ETN=%02x ESN=%02x\n",
                  val, id_data[3], id_data[4]);
        id_dtlh = id_data[1];
        id_etn = id_data[3];
        id_esn = id_data[4];
        id_activate(id_ctlr_unit, ID_SPEC_WAIT);
        break;
    case ID_CMD_SUS:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tSense Unit Status - %d\n",
                  val, id_ua);
        id_activate(id_sel_unit, ID_SUS_WAIT);
        break;
    case ID_CMD_DERR:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tDetect Error\n",
                  val);
        id_activate(id_ctlr_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_RECAL:
        id_buffered = (val & 0x08) == 0x08;
        time = MAX(ID_STEP_MIN, id_state[id].cyl * ID_STEP_WAIT);

        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tRecalibrate - %d - (buffered=%d, polling=%d, delay=%u)\n",
                  val, id_ua, id_buffered, POLLING(id), time);

        if (POLLING(id)) {
            id_set_status(ID_STAT_CEH);
        }

        /* If large disk support hasn't been enabled, and the starting cylinder we're
         * recalibrating from is > 1023, we can't reach track 0! */
        if (!id_large && id_state[id].cyl > 1023) {
            sim_debug(WRITE_MSG, &id_dev,
                      "WARNING: RECALIBRATE - Starting cylinder %d is > 1023!\n",
                      id_state[id].cyl);

            id_state[id].cyl -= 1023;
        } else {
            id_state[id].cyl = 0;
        }

        id_activate(id_sel_unit, time);
        break;
    case ID_CMD_SEEK:
        id_buffered = (val & 0x08) == 0x08;
        id_state[id].lcnh = id_data[0];
        id_state[id].lcnl = id_data[1];
        cyl = ((uint16)id_state[id].lcnh) << 8 | (uint16)id_state[id].lcnl;
        time = MAX(ID_STEP_MIN, ID_STEP_WAIT * (cyl > id_state[id].cyl ? cyl - id_state[id].cyl : id_state[id].cyl - cyl));

        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tSeek - %d - CYL=%u BUF=%d POL=%d DELAY=%u\n",
                  val, id_ua, cyl, id_buffered, POLLING(id), time);

        if (POLLING(id)) {
            id_set_status(ID_STAT_CEH);
        }

        id_state[id].cyl = cyl;
        id_activate(id_sel_unit, time);
        break;
    case ID_CMD_FMT:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tFormat - %d\n",
                  val, id_ua);

        id_state[id].phn  = id_data[0];
        id_state[id].scnt = id_data[1];
        pattern = id_data[2];

        /* Format scnt sectors with the given pattern, if attached */
        if (id_sel_unit->flags & UNIT_ATT) {
            /* Formatting soft-sectored disks always begins at sector 0 */
            sec = 0;

            while (id_state[id].scnt-- > 0) {
                /* Write one sector of pattern */
                for (id_buf_ptr = 0; id_buf_ptr < ID_SEC_SIZE; id_buf_ptr++) {
                    id_buf[id_buf_ptr] = pattern;
                }
                lba = id_lba(id_state[id].cyl, id_state[id].phn, sec++);
                if (sim_disk_wrsect(id_sel_unit, lba, id_buf, NULL, 1) == SCPE_OK) {
                    sim_debug(EXECUTE_MSG, &id_dev,
                              "FORMAT: PHN=%d SCNT=%d PAT=%02x LBA=%04x\n",
                              id_state[id].phn, id_state[id].scnt, pattern, lba);
                } else {
                    sim_debug(EXECUTE_MSG, &id_dev,
                              "FORMAT FAILED! PHN=%d SCNT=%d PAT=%02x LBA=%04x\n",
                              id_state[id].phn, id_state[id].scnt, pattern, lba);
                    break;
                }
            }

            id_data[0] = 0;
        } else {
            /* Not attached */
            id_data[0] = ID_EST_NR;
        }

        id_data[1] = id_state[id].scnt;

        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_VID:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tVerify ID - %d\n",
                  val, id_ua);
        id_data[0] = 0;
        id_data[1] = 0x05; /* What do we put here? */
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_RID:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tRead ID - %d\n",
                  val, id_ua);
        if (id_sel_unit->flags & UNIT_ATT) {
            id_drq = TRUE;

            /* Grab our arguments */
            id_state[id].phn = id_data[0];
            id_state[id].scnt = id_data[1];

            /* Compute logical values used by ID verification */
            id_state[id]. lhn = id_state[id].phn;
            id_state[id].lsn = 0;
        } else {
            sim_debug(EXECUTE_MSG, &id_dev,
                      "UNIT %d NOT ATTACHED, CANNOT READ ID.\n",
                      id_ua);
        }
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_RDIAG:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tRead Diag - %d\n",
                  val, id_ua);
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_RDATA:
        /* Grab our arguments */
        id_state[id].phn  = id_data[0];
        id_state[id].lcnh = ~(id_data[1]);
        id_state[id].lcnl = id_data[2];
        id_state[id].lhn  = id_data[3];
        id_state[id].lsn  = id_data[4];
        id_state[id].scnt = id_data[5];

        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tRead Data - %d - CYL=%u PH=%u LH=%u SEC=%u SCNT=%u\n",
                  val, id_ua,
                  (uint16)id_state[id].lcnh << 8 | (uint16)id_state[id].lcnl,
                  id_data[0], id_data[3], id_data[4], id_data[5]);

        if (id_sel_unit->flags & UNIT_ATT) {
            id_drq = TRUE;
            id_buf_ptr = 0;
        } else {
            sim_debug(EXECUTE_MSG, &id_dev,
                      "UNIT %d NOT ATTACHED, CANNOT READ DATA.\n",
                      id_ua);
        }

        id_activate(id_sel_unit, ID_RW_WAIT);
        break;
    case ID_CMD_CHECK:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tCheck - %d\n",
                  val, id_ua);
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_SCAN:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tScan - %d\n",
                  val, id_ua);
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_VDATA:
        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tVerify Data - %d\n",
                  val, id_ua);
        id_activate(id_sel_unit, ID_CMD_WAIT);
        break;
    case ID_CMD_WDATA:
        /* Grab our arguments */
        id_state[id].phn  = id_data[0];
        id_state[id].lcnh = ~(id_data[1]);
        id_state[id].lcnl = id_data[2];
        id_state[id].lhn  = id_data[3];
        id_state[id].lsn  = id_data[4];
        id_state[id].scnt = id_data[5];

        sim_debug(WRITE_MSG, &id_dev,
                  "COMMAND\t%02x\tWrite Data - %d - CYL=%u PH=%u LH=%u SEC=%u SCNT=%u\n",
                  val, id_ua,
                  (uint16)id_state[id].lcnh << 8 | (uint16)id_state[id].lcnl,
                  id_data[0], id_data[3], id_data[4], id_data[5]);

        if (id_sel_unit->flags & UNIT_ATT) {
            id_drq = TRUE;
            id_buf_ptr = 0;
        } else {
            sim_debug(EXECUTE_MSG, &id_dev,
                      "UNIT %d NOT ATTACHED, CANNOT WRITE.\n",
                      id_ua);
        }
        id_activate(id_sel_unit, ID_RW_WAIT);
        break;
    }
}

void id_after_dma()
{
    id_clr_status(ID_STAT_DRQ);
    id_drq = FALSE;
}

CONST char *id_description(DEVICE *dptr)
{
    return "Integrated Hard Disk";
}

t_stat id_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
{
    fprintf(st, "Integrated Hard Disk (IDISK)\n\n");
    fprintf(st, "The IDISK device implements the integrated MFM hard disk of the\n");
    fprintf(st, "3B2/400. Up to two drives are supported on a single controller.\n\n");
    fprintf(st, "Supported device types are:\n\n");
    fprintf(st, "  Name    Size    ID    Cyl  Head  Sec  Byte/Sec  Description\n");
    fprintf(st, "  ----  --------  --   ----  ----  ---  --------  ----------------------\n");
    fprintf(st, "  HD30   30.6 MB   3    697     5   18    512     CDC Wren 94155-36\n");
    fprintf(st, "  HD72   73.2 MB   5    925     9   18    512     CDC Wren II 94156-86\n");
    fprintf(st, "  HD72C  72.9 MB   8    754    11   18    512     Fujitsu M2243AS\n");
    fprintf(st, "  HD135 135.0 MB  11   1024    15   18    512     Maxtor XT-2190\n");
    if (id_large) {
        fprintf(st, "  HD161 161.4 MB  11   1224    15   18    512     Maxtor XT-2190\n\n");
    }
    fprintf(st, "The drive ID and geometry values are used when low-level formatting a\n");
    fprintf(st, "drive using the AT&T 'idtools' utility.\n");

    fprint_set_help(st, dptr);
    fprint_show_help(st, dptr);
    fprint_reg_help(st, dptr);

    return SCPE_OK;
}
