/*************************************************************************
 *                                                                       *
 * Copyright (c) 2022 Howard M. Harte.                                   *
 * https://github.com/hharte                                             *
 *                                                                       *
 * 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 NON-            *
 * INFRINGEMENT. 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 names of The Authors 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 Authors.                                                     *
 *                                                                       *
 * Based on s100_disk3.c                                                 *
 *                                                                       *
 * Module Description:                                                   *
 *     Morrow Disk Jockey HDC-DMA Hard Disk Controller module for SIMH.  *
 * Reference:                                                            *
 * http://www.bitsavers.org/pdf/morrow/boards/HDC_DMA_Technical_Manual_1983.pdf *
 *                                                                       *
 *************************************************************************/

#include "altairz80_defs.h"
#include "sim_imd.h"

#define DEV_NAME    "DJHDC"

#define DJHDC_MAX_CYLS            1024
#define DJHDC_MAX_HEADS           8
#define DJHDC_MAX_SPT             256

/* Debug flags */
#define ERROR_MSG   (1 << 0)
#define SEEK_MSG    (1 << 1)
#define OPCODE_MSG  (1 << 2)
#define RD_DATA_MSG (1 << 3)
#define WR_DATA_MSG (1 << 4)
#define IRQ_MSG     (1 << 5)
#define VERBOSE_MSG (1 << 6)
#define FORMAT_MSG  (1 << 7)

#define DJHDC_MAX_DRIVES    4

/* DJHDC I/O Ports */
#define DJHDC_RESET     0   /* Reset */
#define DJHDC_START     1   /* Start */

#define DJHDC_LINK_ADDR 0x000050    /* Default link address in RAM */

#define DJHDC_STEP_DIR  0x10        /* Bit 4, Step OUT */

#define DJHDC_IRQ_EN_MASK   0x80    /* Interrupt enable mask */

#define DJHDC_OPCODE_READ_DATA      0x00
#define DJHDC_OPCODE_WRITE_DATA     0x01
#define DJHDC_OPCODE_READ_HEADER    0x02
#define DJHDC_OPCODE_FORMAT_TRACK   0x03
#define DJHDC_OPCODE_LOAD_CONSTANTS 0x04
#define DJHDC_OPCODE_SENSE_STATUS   0x05
#define DJHDC_OPCODE_NOOP           0x06

#define DJHDC_STATUS_BUSY               0x00    /* Busy */
#define DJHDC_STATUS_NOT_READY          0x01    /* Drive not ready */
#define DJHDC_STATUS_HEADER_NOT_FOUND   0x04    /* Sector header not found */
#define DJHDC_STATUS_DATA_NOT_FOUND     0x05    /* Sector data not found */
#define DJHDC_STATUS_DATA_OVERRUN       0x06    /* Data overrun(channel error) */
#define DJHDC_STATUS_DATA_CRC_ERROR     0x07    /* Data CRC error */
#define DJHDC_STATUS_WRITE_FAULT        0x08    /* Write fault */
#define DJHDC_STATUS_HEADER_CRC_ERROR   0x09    /* Sector header CRC error */
#define DJHDC_STATUS_ILLEGAL_COMMAND    0xA0    /* Illegal command */
#define DJHDC_STATUS_COMPLETE           0xFF    /* Successful completion */

#define DJHDC_TRACK_0_DETECT            (1 << 0)    /* 0 = track 0. */
#define DJHDC_WRITE_FAULT_SIGNAL        (1 << 1)    /* Drive Write fault (0). */
#define DJHDC_DRIVE_READY_SIGNAL        (1 << 2)    /* Drive is up to speed (0). */

#define DJHDC_OPCODE_MASK       0x07

#define DJHDC_IOPB_LEN  16

#define DJHDC_IOPB_SELDRV   0
#define DJHDC_IOPB_STEP_L   1
#define DJHDC_IOPB_STEP_H   2
#define DJHDC_IOPB_SEL_HD   3
#define DJHDC_IOPB_DMA_L    4
#define DJHDC_IOPB_DMA_H    5
#define DJHDC_IOPB_DMA_E    6
#define DJHDC_IOPB_ARG0     7
#define DJHDC_IOPB_ARG1     8
#define DJHDC_IOPB_ARG2     9
#define DJHDC_IOPB_ARG3     10
#define DJHDC_IOPB_OPCODE   11
#define DJHDC_IOPB_STATUS   12
#define DJHDC_IOPB_LINK     13
#define DJHDC_IOPB_LINK_H   14
#define DJHDC_IOPB_LINK_E   15

#define DJHDC_INT         1   /* DJHDC interrupts tied to VI1 */

typedef struct {
    UNIT *uptr;
    DISK_INFO *imd;
    uint16 sectsize;    /* sector size, not including pre/postamble */
    uint16 nsectors;    /* number of sectors/track */
    uint16 nheads;      /* number of heads */
    uint16 ntracks;     /* number of tracks */
    uint16 res_tracks;  /* Number of reserved tracks on drive. */
    uint16 track;       /* Current Track */

    uint16 cur_sect;    /* current starting sector of transfer */
    uint16 cur_cyl;     /* Current Track */
    uint16 cur_head;    /* Number of sectors to transfer */
    uint16 cur_sectsize;/* Current sector size */
    uint8 ready;        /* Is drive ready? */
} DJHDC_DRIVE_INFO;

typedef struct {
    PNP_INFO    pnp;    /* Plug and Play */
    uint8   sel_drive;  /* Currently selected drive */
    uint8   mode;       /* mode (0xFF=absolute, 0x00=logical) */
    uint8   ndrives;    /* Number of drives attached to the controller */

    uint32  link_addr;  /* Link Address for next IOPB */
    uint32  dma_addr;   /* DMA Address for the current IOPB */

    uint16  steps;      /* Step count */
    uint8   step_dir;   /* Step direction, 1 = out. */
    uint8   irq_enable;
    uint8   step_delay;
    uint8   head_settle_time;
    uint8   sector_size_code;

    DJHDC_DRIVE_INFO drive[DJHDC_MAX_DRIVES];
    uint8   iopb[16];
} DJHDC_INFO;

static DJHDC_INFO djhdc_info_data = { { 0x0, 0, 0x54, 2 } };
static DJHDC_INFO *djhdc_info = &djhdc_info_data;

/* Disk geometries:
 *            IMI       SCRIBE
 *  Sectsize: 1024      1024
 *   Sectors: 8         8
 *     Heads: 6         4
 *    Tracks: 306       480
 */

/* Default geometry for a 15MB hard disk. */
#define SCRIBE_SECTSIZE  1024
#define SCRIBE_NSECTORS  8
#define SCRIBE_NHEADS    4
#define SCRIBE_NTRACKS   480

static const char* djhdc_opcode_str[] = {
    "Read Data     ",
    "Write Data    ",
    "Read Header   ",
    "Format Track  ",
    "Load Constants",
    "Sense Status  ",
    "No Operation  ",
    "Invalid       "
};

static int32 ntracks      = SCRIBE_NTRACKS;
static int32 nheads       = SCRIBE_NHEADS;
static int32 nsectors     = SCRIBE_NSECTORS;
static int32 sectsize     = SCRIBE_SECTSIZE;

extern uint32 PCX;
extern t_stat set_iobase(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
extern t_stat show_iobase(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
extern uint32 sim_map_resource(uint32 baseaddr, uint32 size, uint32 resource_type,
                               int32 (*routine)(const int32, const int32, const int32), const char* name, uint8 unmap);
extern int32 find_unit_index(UNIT *uptr);
extern void raise_scp300f_interrupt(uint8 intnum);

/* These are needed for DMA. */
extern void PutByteDMA(const uint32 Addr, const uint32 Value);
extern uint8 GetByteDMA(const uint32 Addr);

#define UNIT_V_DJHDC_VERBOSE    (UNIT_V_UF + 1) /* verbose mode, i.e. show error messages   */
#define UNIT_DJHDC_VERBOSE      (1 << UNIT_V_DJHDC_VERBOSE)
#define DJHDC_CAPACITY          (SCRIBE_NTRACKS * SCRIBE_NHEADS * \
                                 SCRIBE_NSECTORS * SCRIBE_SECTSIZE)   /* Default Disk Capacity */

static t_stat djhdc_reset(DEVICE *djhdc_dev);
static t_stat djhdc_attach(UNIT *uptr, CONST char *cptr);
static t_stat djhdc_detach(UNIT *uptr);
static t_stat djhdc_unit_set_geometry(UNIT* uptr, int32 value, CONST char* cptr, void* desc);
static t_stat djhdc_unit_show_geometry(FILE* st, UNIT* uptr, int32 value, CONST void* desc);
static int DJHDC_Validate_CHSN(DJHDC_DRIVE_INFO* pDrive);
#ifdef DJHDC_INTERRUPTS
static void raise_djhdc_interrupt(void);
#endif /* DJHDC_INTERRUPTS */

static const char* djhdc_description(DEVICE *dptr);

static int32 djhdcdev(const int32 port, const int32 io, const int32 data);

/* static uint8 DJHDC_Read(const uint32 Addr); */
static uint8 DJHDC_Write(const uint32 Addr, uint8 cData);

static UNIT djhdc_unit[] = {
    { UDATA (NULL, UNIT_FIX + UNIT_ATTABLE + UNIT_DISABLE + UNIT_ROABLE, DJHDC_CAPACITY) },
    { UDATA (NULL, UNIT_FIX + UNIT_ATTABLE + UNIT_DISABLE + UNIT_ROABLE, DJHDC_CAPACITY) },
    { UDATA (NULL, UNIT_FIX + UNIT_ATTABLE + UNIT_DISABLE + UNIT_ROABLE, DJHDC_CAPACITY) },
    { UDATA (NULL, UNIT_FIX + UNIT_ATTABLE + UNIT_DISABLE + UNIT_ROABLE, DJHDC_CAPACITY) }
};

static REG djhdc_reg[] = {
    { DRDATAD (NTRACKS,    ntracks,                     10,
               "Number of tracks"),                             },
    { DRDATAD (NHEADS,     nheads,                      8,
               "Number of heads"),                              },
    { DRDATAD (NSECTORS,   nsectors,                    8,
               "Number of sectors per track"),                  },
    { DRDATAD (SECTSIZE,   sectsize,                    11,
               "Sector size not including pre/postamble"),      },
    { HRDATAD (SEL_DRIVE,  djhdc_info_data.sel_drive,   3,
               "Currently selected drive"),                     },
    { HRDATAD (MODE,       djhdc_info_data.mode,        8,
               "Mode (0xFF=absolute, 0x00=logical)"),           },
    { HRDATAD (NDRIVES,    djhdc_info_data.ndrives,     8,
               "Number of drives attached to the controller"),  },
    { HRDATAD (LINK_ADDR,  djhdc_info_data.link_addr,   32,
               "Link address for next IOPB"),                   },
    { HRDATAD (DMA_ADDR,   djhdc_info_data.dma_addr,    32,
               "DMA address for the current IOPB"),             },
    { BRDATAD (IOPB,       djhdc_info_data.iopb,       16, 8, 16,
               "IOPB command register"),                        },
    { NULL }
};

#define DJHDC_NAME  "Morrow HDC/DMA Hard Disk Controller"

static const char* djhdc_description(DEVICE *dptr) {
    if (dptr == NULL) {
        return NULL;
    }
    return DJHDC_NAME;
}

static MTAB djhdc_mod[] = {
    { MTAB_XTD|MTAB_VDV,    0,                  "IOBASE",   "IOBASE",
        &set_iobase, &show_iobase, NULL, "Sets disk controller I/O base address"    },
    { MTAB_XTD | MTAB_VUN | MTAB_VALR,    0,                  "GEOMETRY",     "GEOMETRY",
        &djhdc_unit_set_geometry, &djhdc_unit_show_geometry, NULL,
        "Set disk geometry C:nnnn/H:n/S:nnn/N:nnnn" },
    { 0 }
};

/* Debug Flags */
static DEBTAB djhdc_dt[] = {
    { "ERROR",      ERROR_MSG,      "Error messages"    },
    { "SEEK",       SEEK_MSG,       "Seek messages"     },
    { "OPCODE",     OPCODE_MSG,     "Opcode messages"   },
    { "READ",       RD_DATA_MSG,    "Read messages"     },
    { "WRITE",      WR_DATA_MSG,    "Write messages"    },
    { "IRQ",        IRQ_MSG,        "IRQ messages"      },
    { "VERBOSE",    VERBOSE_MSG,    "Verbose messages"  },
    { "FORMAT",     FORMAT_MSG,     "Format messages"  },
    { NULL,         0                                   }
};

DEVICE djhdc_dev = {
    DEV_NAME, djhdc_unit, djhdc_reg, djhdc_mod,
    DJHDC_MAX_DRIVES, 10, 31, 1, DJHDC_MAX_DRIVES, DJHDC_MAX_DRIVES,
    NULL, NULL, &djhdc_reset,
    NULL, &djhdc_attach, &djhdc_detach,
    &djhdc_info_data, (DEV_DISABLE | DEV_DIS | DEV_DEBUG), ERROR_MSG,
    djhdc_dt, NULL, NULL, NULL, NULL, NULL, &djhdc_description
};

/* Reset routine */
static t_stat djhdc_reset(DEVICE *dptr)
{
    PNP_INFO *pnp = (PNP_INFO *)dptr->ctxt;

    if(dptr->flags & DEV_DIS) { /* Disconnect I/O Ports */
        sim_map_resource(pnp->io_base, pnp->io_size, RESOURCE_TYPE_IO, &djhdcdev, "djhdcdev", TRUE);
    } else {
        /* Connect DJHDC at base address */
        if(sim_map_resource(pnp->io_base, pnp->io_size, RESOURCE_TYPE_IO, &djhdcdev, "djhdcdev", FALSE) != 0) {
            sim_printf("%s: error mapping I/O resource at 0x%04x\n", __FUNCTION__, pnp->io_base);
            return SCPE_ARG;
        }
    }

    djhdc_info->link_addr = DJHDC_LINK_ADDR;   /* After RESET, the link pointer is at 0x000050. */

    return SCPE_OK;
}


/* Attach routine */
static t_stat djhdc_attach(UNIT *uptr, CONST char *cptr)
{
    t_stat r = SCPE_OK;
    DJHDC_DRIVE_INFO *pDrive;
    int i = 0;

    i = find_unit_index(uptr);
    if (i == -1) {
        return (SCPE_IERR);
    }
    pDrive = &djhdc_info->drive[i];

    pDrive->ready = 1;
    pDrive->track = 5;

    if (pDrive->ntracks == 0) {
        /* If geometry was not specified, default to Miniscribe 15MB */
        pDrive->ntracks = SCRIBE_NTRACKS;
        pDrive->nheads = SCRIBE_NHEADS;
        pDrive->nsectors = SCRIBE_NSECTORS;
        pDrive->sectsize = SCRIBE_SECTSIZE;
    }

    r = attach_unit(uptr, cptr);    /* attach unit  */
    if ( r != SCPE_OK)              /* error?       */
        return r;

    /* Determine length of this disk */
    if(sim_fsize(uptr->fileref) != 0) {
        uptr->capac = sim_fsize(uptr->fileref);
    } else {
        uptr->capac = (pDrive->ntracks * pDrive->nsectors * pDrive->nheads * pDrive->sectsize);
    }

    pDrive->uptr = uptr;

    /* Default for new file is DSK */
    uptr->u3 = IMAGE_TYPE_DSK;

    if(uptr->capac > 0) {
        r = assignDiskType(uptr);
        if (r != SCPE_OK) {
            djhdc_detach(uptr);
            return r;
        }
    }

    if (uptr->flags & UNIT_DJHDC_VERBOSE)
        sim_printf("DJHDC%d, attached to '%s', type=%s, len=%d\n", i, cptr,
            uptr->u3 == IMAGE_TYPE_IMD ? "IMD" : uptr->u3 == IMAGE_TYPE_CPT ? "CPT" : "DSK",
            uptr->capac);

    if(uptr->u3 == IMAGE_TYPE_IMD) {
        if(uptr->capac < 318000) {
            sim_printf("Cannot create IMD files with SIMH.\nCopy an existing file and format it with CP/M.\n");
            djhdc_detach(uptr);
            return SCPE_OPENERR;
        }

        if (uptr->flags & UNIT_DJHDC_VERBOSE)
            sim_printf("--------------------------------------------------------\n");
        djhdc_info->drive[i].imd = diskOpenEx((uptr->fileref), (uptr->flags & UNIT_DJHDC_VERBOSE),
                                              &djhdc_dev, VERBOSE_MSG, VERBOSE_MSG);
        if (uptr->flags & UNIT_DJHDC_VERBOSE)
            sim_printf("\n");
    } else {
        djhdc_info->drive[i].imd = NULL;
    }

    return SCPE_OK;
}


/* Detach routine */
static t_stat djhdc_detach(UNIT *uptr)
{
    DJHDC_DRIVE_INFO *pDrive;
    t_stat r;
    int32 i;

    i = find_unit_index(uptr);

    if (i == -1) {
        return (SCPE_IERR);
    }

    pDrive = &djhdc_info->drive[i];

    pDrive->ready = 0;

    if (uptr->flags & UNIT_DJHDC_VERBOSE)
        sim_printf("Detach DJHDC%d\n", i);

    r = detach_unit(uptr);  /* detach unit */
    if ( r != SCPE_OK)
        return r;

    return SCPE_OK;
}

/* Set geometry of the disk drive */
static t_stat djhdc_unit_set_geometry(UNIT* uptr, int32 value, CONST char* cptr, void* desc)
{
    DJHDC_DRIVE_INFO* pDrive;
    int32 i;
    int32 result;
    uint16 newCyls, newHeads, newSPT, newSecLen;

    i = find_unit_index(uptr);

    if (i == -1) {
        return (SCPE_IERR);
    }

    pDrive = &djhdc_info->drive[i];

    if (cptr == NULL)
        return SCPE_ARG;

    result = sscanf(cptr, "C:%hd/H:%hd/S:%hd/N:%hd", &newCyls, &newHeads, &newSPT, &newSecLen);
    if (result != 4)
        return SCPE_ARG;

    /* Validate Cyl, Heads, Sector, Length are valid for the HDC-1001 */
    if (newCyls < 1 || newCyls > DJHDC_MAX_CYLS) {
        sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "%d: Number of cylinders must be 1-%d.\n",
            djhdc_info->sel_drive, DJHDC_MAX_CYLS);
        return SCPE_ARG;
    }
    if (newHeads < 1 || newHeads > DJHDC_MAX_HEADS) {
        sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "%d: Number of heads must be 1-%d.\n",
            djhdc_info->sel_drive, DJHDC_MAX_HEADS);
        return SCPE_ARG;
    }
    if (newSPT < 1 || newSPT > DJHDC_MAX_SPT) {
        sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "%d: Number of sectors per track must be 1-%d.\n",
            djhdc_info->sel_drive, DJHDC_MAX_SPT);
        return SCPE_ARG;
    }
    if (newSecLen != 2048 && newSecLen != 1024 && newSecLen != 512 && newSecLen != 256 && newSecLen != 128) {
        sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "%d: Sector length must be 128, 256, 512, 1024, or 2048.\n",
            djhdc_info->sel_drive);
        return SCPE_ARG;
    }

    pDrive->ntracks = newCyls;
    pDrive->nheads = newHeads;
    pDrive->nsectors = newSPT;
    pDrive->sectsize = newSecLen;

    return SCPE_OK;
}

/* Show geometry of the disk drive */
static t_stat djhdc_unit_show_geometry(FILE* st, UNIT* uptr, int32 value, CONST void* desc)
{
    DJHDC_DRIVE_INFO* pDrive;
    int32 i;

    i = find_unit_index(uptr);

    if (i == -1) {
        return (SCPE_IERR);
    }

    pDrive = &djhdc_info->drive[i];

    fprintf(st, "C:%d/H:%d/S:%d/N:%d",
        pDrive->ntracks, pDrive->nheads, pDrive->nsectors, pDrive->sectsize);

    return SCPE_OK;
}

static int32 djhdcdev(const int32 port, const int32 io, const int32 data)
{
    sim_debug(VERBOSE_MSG, &djhdc_dev, DEV_NAME ": " ADDRESS_FORMAT
              " IO %s, Port %02x\n", PCX, io ? "WR" : "RD", port);
    if(io) {
        DJHDC_Write(port, (uint8)data);
        return 0;
    } else {
        return(0xFF);
    }
}

static uint8 DJHDC_Write(const uint32 Addr, uint8 cData)
{
    uint32 next_link;
    uint8 result = DJHDC_STATUS_COMPLETE;
    uint8 i;
    uint8 opcode;

    DJHDC_DRIVE_INFO *pDrive;

    /* RESET */
    if ((Addr & 1) == DJHDC_RESET) {
        djhdc_info->link_addr = DJHDC_LINK_ADDR - DJHDC_IOPB_LINK;

        sim_debug(VERBOSE_MSG, &djhdc_dev, DEV_NAME "[%d]: RESET\n",
            djhdc_info->sel_drive);

        return 0;
    }

    /* START */

    /* Read first three bytes of IOPB (link address) */
    for (i = DJHDC_IOPB_LINK; i < DJHDC_IOPB_LEN; i++) {
        djhdc_info->iopb[i] = GetByteDMA((djhdc_info->link_addr) + i);
    }

    next_link  = djhdc_info->iopb[DJHDC_IOPB_LINK + 0];
    next_link |= djhdc_info->iopb[DJHDC_IOPB_LINK+1] << 8;
    next_link |= djhdc_info->iopb[DJHDC_IOPB_LINK+2] << 16;

    /* Point IOPB to new link */
    djhdc_info->link_addr = next_link;

    /* Read remainder of IOPB */
    for(i = 0; i < DJHDC_IOPB_LEN-3; i++) {
        djhdc_info->iopb[i] = GetByteDMA((djhdc_info->link_addr) + i);
    }

    /* Process the IOPB */
    djhdc_info->iopb[DJHDC_IOPB_OPCODE] = djhdc_info->iopb[DJHDC_IOPB_OPCODE] & DJHDC_OPCODE_MASK;

    opcode = djhdc_info->iopb[DJHDC_IOPB_OPCODE];
    djhdc_info->sel_drive = djhdc_info->iopb[DJHDC_IOPB_SELDRV] & 0x03;
    djhdc_info->step_dir = (djhdc_info->iopb[DJHDC_IOPB_SELDRV] & DJHDC_STEP_DIR) ? 1 : 0;

    djhdc_info->steps  = djhdc_info->iopb[DJHDC_IOPB_STEP_L];
    djhdc_info->steps |= djhdc_info->iopb[DJHDC_IOPB_STEP_H] << 8;

    djhdc_info->dma_addr  = djhdc_info->iopb[DJHDC_IOPB_DMA_L];
    djhdc_info->dma_addr |= djhdc_info->iopb[DJHDC_IOPB_DMA_H] << 8;
    djhdc_info->dma_addr |= djhdc_info->iopb[DJHDC_IOPB_DMA_E] << 16;

    sim_debug(VERBOSE_MSG, &djhdc_dev, DEV_NAME "[%d]: SEEK=%d %s, LINK=0x%05x, OPCODE=%x, %s DMA@0x%05x\n",
              djhdc_info->sel_drive,
              djhdc_info->steps,
              djhdc_info->step_dir ? "OUT" : "IN",
              djhdc_info->link_addr,
              djhdc_info->iopb[DJHDC_IOPB_OPCODE],
              djhdc_opcode_str[djhdc_info->iopb[DJHDC_IOPB_OPCODE]],
              djhdc_info->dma_addr);

    pDrive = &djhdc_info->drive[djhdc_info->sel_drive];

    if(pDrive->ready) {
        /* Seek phase */
        if (djhdc_info->step_dir) {
            /* Step Out */
            if (djhdc_info->steps >= pDrive->cur_cyl) {
                pDrive->cur_cyl = 0;
                sim_debug(SEEK_MSG, &djhdc_dev, DEV_NAME "[%d]: HOME\n",
                    djhdc_info->sel_drive);
            }
            else {
                pDrive->cur_cyl -= djhdc_info->steps;
            }
        }
        else {
            /* Step In */
            pDrive->cur_cyl += djhdc_info->steps;
        }

        sim_debug(SEEK_MSG, &djhdc_dev, DEV_NAME "[%d]: Current track: %d\n",
            djhdc_info->sel_drive,
            pDrive->cur_cyl);


        /* Perform command */
        switch(opcode) {
            case DJHDC_OPCODE_READ_DATA:
            case DJHDC_OPCODE_WRITE_DATA:
            {
                uint32 track_len;
                uint32 xfr_len;
                uint32 file_offset;
                uint32 xfr_count = 0;
                uint8* dataBuffer;
                size_t rtn;

                pDrive->cur_cyl = djhdc_info->iopb[DJHDC_IOPB_ARG0] | (djhdc_info->iopb[DJHDC_IOPB_ARG1] << 8);
                pDrive->cur_head = djhdc_info->iopb[DJHDC_IOPB_ARG2];
                pDrive->cur_sect = djhdc_info->iopb[DJHDC_IOPB_ARG3] - 1;

                if (DJHDC_Validate_CHSN(pDrive) != SCPE_OK) {
                    result = DJHDC_STATUS_HEADER_NOT_FOUND;
                    break;
                }

                track_len = pDrive->nsectors * pDrive->nheads * pDrive->sectsize;

                file_offset  = (pDrive->cur_cyl * track_len); /* Calculate offset based on current track */
                file_offset += pDrive->nsectors * pDrive->cur_head * pDrive->sectsize;
                file_offset += pDrive->cur_sect * pDrive->sectsize;

                xfr_len = pDrive->sectsize;

                dataBuffer = (uint8*)malloc(xfr_len);
                if (dataBuffer == NULL) {
                    sim_printf("%s: error allocating memory\n", __FUNCTION__);
                    return (0);
                }

                if (sim_fseek((pDrive->uptr)->fileref, file_offset, SEEK_SET) == 0) {

                    if (opcode == DJHDC_OPCODE_READ_DATA) { /* Read */
                        rtn = sim_fread(dataBuffer, 1, xfr_len, (pDrive->uptr)->fileref);

                        sim_debug(RD_DATA_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                            "  READ @0x%05x C:%04d/H:%d/S:%04d len=%d, file_offset=%d, %s\n",
                            djhdc_info->sel_drive,
                            PCX,
                            djhdc_info->dma_addr,
                            pDrive->cur_cyl,
                            pDrive->cur_head,
                            pDrive->cur_sect,
                            xfr_len,
                            file_offset,
                            rtn == (size_t)xfr_len ? "OK" : "NOK");


                        /* Perform DMA Transfer */
                        for (xfr_count = 0; xfr_count < xfr_len; xfr_count++) {
                            PutByteDMA(djhdc_info->dma_addr + xfr_count, dataBuffer[xfr_count]);
                        }
                    }
                    else { /* Write */
                        sim_debug(WR_DATA_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                            " WRITE @0x%05x C:%04d/H:%d/S:%04d file_offset=%d, len=%d\n",
                            djhdc_info->sel_drive,
                            PCX, djhdc_info->dma_addr,
                            pDrive->cur_cyl,
                            pDrive->cur_head,
                            pDrive->cur_sect,
                            file_offset,
                            xfr_len);

                        /* Perform DMA Transfer */
                        for (xfr_count = 0; xfr_count < xfr_len; xfr_count++) {
                            dataBuffer[xfr_count] = GetByteDMA(djhdc_info->dma_addr + xfr_count);
                        }

                        sim_fwrite(dataBuffer, 1, xfr_len, (pDrive->uptr)->fileref);
                    }
                }
                else {
                    sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT " READWRITE: sim_fseek error.\n", djhdc_info->sel_drive, PCX);
                }

                free(dataBuffer);

                break;
            }
            case DJHDC_OPCODE_READ_HEADER:
                sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT " READ_HEADER: not implemented.\n", djhdc_info->sel_drive, PCX);
                result = DJHDC_STATUS_HEADER_NOT_FOUND;
                break;
            case DJHDC_OPCODE_FORMAT_TRACK:
            {
                uint32  track_len;
                uint32  file_offset;
                uint8*  fmtBuffer;
                uint8   head;
                uint8   gap;
                uint8   sector_count;
                uint8   sector_size_code;
                uint8   fill_byte;

                head = ~(djhdc_info->iopb[DJHDC_IOPB_SEL_HD] >> 2) & 7;
                gap = djhdc_info->iopb[DJHDC_IOPB_ARG0];
                sector_count = 255 - djhdc_info->iopb[DJHDC_IOPB_ARG1];
                sector_size_code = djhdc_info->iopb[DJHDC_IOPB_ARG2];
                fill_byte = djhdc_info->iopb[DJHDC_IOPB_ARG3];

                switch (sector_size_code) {
                case 0xFF:
                    pDrive->cur_sectsize = 128;
                    break;
                case 0xFE:
                    pDrive->cur_sectsize = 256;
                    break;
                case 0xFC:
                    pDrive->cur_sectsize = 512;
                    break;
                case 0xF8:
                    pDrive->cur_sectsize = 1024;
                    break;
                case 0xF0:
                    pDrive->cur_sectsize = 2048;
                    break;
                default:
                    sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME ": Invalid sector size code: 0x%02x.\n",
                        djhdc_info->sector_size_code);
                    pDrive->cur_sectsize = 0;
                    result = DJHDC_STATUS_ILLEGAL_COMMAND;
                    break;
                }

                if (DJHDC_Validate_CHSN(pDrive) != SCPE_OK) {
                    result = DJHDC_STATUS_HEADER_NOT_FOUND;
                    break;
                }

                track_len = pDrive->nheads * sector_count * pDrive->sectsize;

                file_offset  = pDrive->cur_cyl * track_len; /* Calculate offset based on current track */
                file_offset += head * sector_count * pDrive->sectsize;

                sim_debug(FORMAT_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                    " FORMAT C:%d/H:%d, Gap=%d, Fill=0x%02x, Count=%d, Sector Size:=%d, file offset: 0x%08x\n",
                    djhdc_info->sel_drive,
                    PCX,
                    pDrive->cur_cyl,
                    head,
                    gap,
                    fill_byte,
                    sector_count,
                    pDrive->sectsize,
                    file_offset);

                fmtBuffer = (uint8*)malloc(track_len);

                if (fmtBuffer == NULL) {
                    sim_printf("%s: error allocating memory\n", __FUNCTION__);
                    return (0);
                }

                memset(fmtBuffer, fill_byte, track_len);

                if (sim_fseek((pDrive->uptr)->fileref, file_offset, SEEK_SET) == 0) {
                    sim_fwrite(fmtBuffer, 1, track_len, (pDrive->uptr)->fileref);
                }
                else {
                    sim_debug(WR_DATA_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT " FORMAT: sim_fseek error.\n", djhdc_info->sel_drive, PCX);
                    result = DJHDC_STATUS_WRITE_FAULT;
                }

                free(fmtBuffer);

                break;
            }
        case DJHDC_OPCODE_LOAD_CONSTANTS:
                djhdc_info->irq_enable = (djhdc_info->iopb[DJHDC_IOPB_ARG1] & DJHDC_IRQ_EN_MASK) ? 1 : 0;
                djhdc_info->step_delay = djhdc_info->iopb[DJHDC_IOPB_ARG1] & ~DJHDC_IRQ_EN_MASK;
                djhdc_info->head_settle_time = djhdc_info->iopb[DJHDC_IOPB_ARG2];
                djhdc_info->sector_size_code = djhdc_info->iopb[DJHDC_IOPB_ARG3];

                switch (djhdc_info->sector_size_code) {
                case 0x00:
                    pDrive->cur_sectsize = 128;
                    break;
                case 0x01:
                    pDrive->cur_sectsize = 256;
                    break;
                case 0x03:
                    pDrive->cur_sectsize = 512;
                    break;
                case 0x07:
                    pDrive->cur_sectsize = 1024;
                    break;
                case 0x0F:
                    pDrive->cur_sectsize = 2048;
                    break;
                default:
                    sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME ": Invalid sector size code: 0x%02x.\n",
                        djhdc_info->sector_size_code);
                    pDrive->cur_sectsize = 0;
                    result = DJHDC_STATUS_ILLEGAL_COMMAND;
                    break;
                }

                sim_debug(VERBOSE_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                    " Load Constants: Interrupt Enable: %d, step delay: %d, head settle time: %d, sector size %d (code: 0x%02x)\n",
                    djhdc_info->sel_drive, PCX,
                    djhdc_info->irq_enable,
                    djhdc_info->step_delay,
                    djhdc_info->head_settle_time,
                    pDrive->sectsize,
                    djhdc_info->sector_size_code);
                break;
        case DJHDC_OPCODE_SENSE_STATUS:
            sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT " SENSE_STATUS: not implemented.\n", djhdc_info->sel_drive, PCX);
            result = DJHDC_DRIVE_READY_SIGNAL;
            if (pDrive->cur_cyl != 0) result = DJHDC_TRACK_0_DETECT;
            break;
        case DJHDC_OPCODE_NOOP:
            sim_debug(VERBOSE_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                        " NOOP\n", djhdc_info->sel_drive, PCX);
            break;

        default:
            sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "[%d]: " ADDRESS_FORMAT
                        " OPCODE=%x Unsupported\n",
                        djhdc_info->sel_drive,
                        PCX,
                        opcode & DJHDC_OPCODE_MASK);
            result = DJHDC_STATUS_ILLEGAL_COMMAND;
            break;
        }
    } else { /* Drive not ready */
        result = DJHDC_STATUS_NOT_READY;
    }
    /* Return status */
    djhdc_info->iopb[DJHDC_IOPB_STATUS] = result;

    /* Update IOPB in host memory */
    PutByteDMA(djhdc_info->link_addr + DJHDC_IOPB_STATUS, djhdc_info->iopb[DJHDC_IOPB_STATUS]);

#ifdef DJHDC_INTERRUPTS
    if(djhdc_info->irq_enable) {
        raise_djhdc_interrupt();
    }
#endif /* DJHDC_INTERRUPTS */

    return 0;
}

/* Validate that Cyl, Head, Sector, Sector Length are valid for the current
 * disk drive geometry.
 */
static int DJHDC_Validate_CHSN(DJHDC_DRIVE_INFO* pDrive)
{
    int status = SCPE_OK;

    /* Check to make sure we're operating on a valid C/H/S/N. */
    if ((pDrive->cur_cyl >= pDrive->ntracks) ||
        (pDrive->cur_head >= pDrive->nheads) ||
        (pDrive->cur_sect >= pDrive->nsectors) ||
        (pDrive->cur_sectsize != pDrive->sectsize))
    {

        sim_debug(ERROR_MSG, &djhdc_dev, DEV_NAME "%d: " ADDRESS_FORMAT
            " ID Not Found (check disk geometry.)\n", djhdc_info->sel_drive, PCX);

        status = SCPE_IOERR;
    }

    return (status);
}

#ifdef DJHDC_INTERRUPTS
static void raise_djhdc_interrupt(void)
{
    sim_debug(IRQ_MSG, &djhdc_dev, DEV_NAME ": " ADDRESS_FORMAT " Interrupt\n", PCX);

    raise_scp300f_interrupt(DJHDC_INT);
}
#endif /* DJHDC_INTERRUPTS */
