/*
 * Copyright (c) 2004-2005 The Regents of The University of Michigan
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution;
 * neither the name of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @file
 * Device model implementation for an IDE disk
 */

#include <cerrno>
#include <cstring>
#include <deque>
#include <string>

#include "base/cprintf.hh" // csprintf
#include "base/trace.hh"
#include "dev/disk_image.hh"
#include "dev/ide_disk.hh"
#include "dev/ide_ctrl.hh"
#include "dev/tsunami.hh"
#include "dev/tsunami_pchip.hh"
#include "mem/functional/physical.hh"
#include "mem/bus/bus.hh"
#include "mem/bus/dma_interface.hh"
#include "mem/bus/pio_interface.hh"
#include "mem/bus/pio_interface_impl.hh"
#include "sim/builder.hh"
#include "sim/sim_object.hh"
#include "sim/root.hh"
#include "arch/isa_traits.hh"

using namespace std;
using namespace TheISA;

IdeDisk::IdeDisk(const string &name, DiskImage *img, PhysicalMemory *phys,
                 int id, Tick delay)
    : SimObject(name), ctrl(NULL), image(img), physmem(phys), diskDelay(delay),
      dmaTransferEvent(this), dmaReadWaitEvent(this),
      dmaWriteWaitEvent(this), dmaPrdReadEvent(this),
      dmaReadEvent(this), dmaWriteEvent(this)
{
    // Reset the device state
    reset(id);

    // fill out the drive ID structure
    memset(&driveID, 0, sizeof(struct ataparams));

    // Calculate LBA and C/H/S values
    uint16_t cylinders;
    uint8_t heads;
    uint8_t sectors;

    uint32_t lba_size = image->size();
    if (lba_size >= 16383*16*63) {
        cylinders = 16383;
        heads = 16;
        sectors = 63;
    } else {
        if (lba_size >= 63)
            sectors = 63;
        else
            sectors = lba_size;

        if ((lba_size / sectors) >= 16)
            heads = 16;
        else
            heads = (lba_size / sectors);

        cylinders = lba_size / (heads * sectors);
    }

    // Setup the model name
    strncpy((char *)driveID.atap_model, "5MI EDD si k",
            sizeof(driveID.atap_model));
    // Set the maximum multisector transfer size
    driveID.atap_multi = MAX_MULTSECT;
    // IORDY supported, IORDY disabled, LBA enabled, DMA enabled
    driveID.atap_capabilities1 = 0x7;
    // UDMA support, EIDE support
    driveID.atap_extensions = 0x6;
    // Setup default C/H/S settings
    driveID.atap_cylinders = cylinders;
    driveID.atap_sectors = sectors;
    driveID.atap_heads = heads;
    // Setup the current multisector transfer size
    driveID.atap_curmulti = MAX_MULTSECT;
    driveID.atap_curmulti_valid = 0x1;
    // Number of sectors on disk
    driveID.atap_capacity = lba_size;
    // Multiword DMA mode 2 and below supported
    driveID.atap_dmamode_supp = 0x400;
    // Set PIO mode 4 and 3 supported
    driveID.atap_piomode_supp = 0x3;
    // Set DMA mode 4 and below supported
    driveID.atap_udmamode_supp = 0x1f;
    // Statically set hardware config word
    driveID.atap_hwreset_res = 0x4001;

    //arbitrary for now...
    driveID.atap_ata_major = WDC_VER_ATA7;
}

IdeDisk::~IdeDisk()
{
    // destroy the data buffer
    delete [] dataBuffer;
}

void
IdeDisk::reset(int id)
{
    // initialize the data buffer and shadow registers
    dataBuffer = new uint8_t[MAX_DMA_SIZE];

    memset(dataBuffer, 0, MAX_DMA_SIZE);
    memset(&cmdReg, 0, sizeof(CommandReg_t));
    memset(&curPrd.entry, 0, sizeof(PrdEntry_t));

    dmaInterfaceBytes = 0;
    curPrdAddr = 0;
    curSector = 0;
    cmdBytes = 0;
    cmdBytesLeft = 0;
    drqBytesLeft = 0;
    dmaRead = false;
    intrPending = false;

    // set the device state to idle
    dmaState = Dma_Idle;

    if (id == DEV0) {
        devState = Device_Idle_S;
        devID = DEV0;
    } else if (id == DEV1) {
        devState = Device_Idle_NS;
        devID = DEV1;
    } else {
        panic("Invalid device ID: %#x\n", id);
    }

    // set the device ready bit
    status = STATUS_DRDY_BIT;

    /* The error register must be set to 0x1 on start-up to
       indicate that no diagnostic error was detected */
    cmdReg.error = 0x1;
}

////
// Utility functions
////

bool
IdeDisk::isDEVSelect()
{
    return ctrl->isDiskSelected(this);
}

Addr
IdeDisk::pciToDma(Addr pciAddr)
{
    if (ctrl)
        return ctrl->plat->pciToDma(pciAddr);
    else
        panic("Access to unset controller!\n");
}

uint32_t
IdeDisk::bytesInDmaPage(Addr curAddr, uint32_t bytesLeft)
{
    uint32_t bytesInPage = 0;

    // First calculate how many bytes could be in the page
    if (bytesLeft > TheISA::PageBytes)
        bytesInPage = TheISA::PageBytes;
    else
        bytesInPage = bytesLeft;

    // Next, see if we have crossed a page boundary, and adjust
    Addr upperBound = curAddr + bytesInPage;
    Addr pageBound = TheISA::TruncPage(curAddr) + TheISA::PageBytes;

    assert(upperBound >= curAddr && "DMA read wraps around address space!\n");

    if (upperBound >= pageBound)
        bytesInPage = pageBound - curAddr;

    return bytesInPage;
}

////
// Device registers read/write
////

void
IdeDisk::read(const Addr &offset, IdeRegType reg_type, uint8_t *data)
{
    DevAction_t action = ACT_NONE;

    switch (reg_type) {
      case COMMAND_BLOCK:
        switch (offset) {
          // Data transfers occur two bytes at a time
          case DATA_OFFSET:
            *(uint16_t*)data = cmdReg.data;
            action = ACT_DATA_READ_SHORT;
            break;
          case ERROR_OFFSET:
            *data = cmdReg.error;
            break;
          case NSECTOR_OFFSET:
            *data = cmdReg.sec_count;
            break;
          case SECTOR_OFFSET:
            *data = cmdReg.sec_num;
            break;
          case LCYL_OFFSET:
            *data = cmdReg.cyl_low;
            break;
          case HCYL_OFFSET:
            *data = cmdReg.cyl_high;
            break;
          case DRIVE_OFFSET:
            *data = cmdReg.drive;
            break;
          case STATUS_OFFSET:
            *data = status;
            action = ACT_STAT_READ;
            break;
          default:
            panic("Invalid IDE command register offset: %#x\n", offset);
        }
        break;
      case CONTROL_BLOCK:
        if (offset == ALTSTAT_OFFSET)
            *data = status;
        else
            panic("Invalid IDE control register offset: %#x\n", offset);
        break;
      default:
        panic("Unknown register block!\n");
    }

    if (action != ACT_NONE)
        updateState(action);
}

void
IdeDisk::write(const Addr &offset, IdeRegType reg_type, const uint8_t *data)
{
    DevAction_t action = ACT_NONE;

    switch (reg_type) {
      case COMMAND_BLOCK:
        switch (offset) {
          case DATA_OFFSET:
            cmdReg.data = *(uint16_t*)data;
            action = ACT_DATA_WRITE_SHORT;
            break;
          case FEATURES_OFFSET:
            break;
          case NSECTOR_OFFSET:
            cmdReg.sec_count = *data;
            break;
          case SECTOR_OFFSET:
            cmdReg.sec_num = *data;
            break;
          case LCYL_OFFSET:
            cmdReg.cyl_low = *data;
            break;
          case HCYL_OFFSET:
            cmdReg.cyl_high = *data;
            break;
          case DRIVE_OFFSET:
            cmdReg.drive = *data;
            action = ACT_SELECT_WRITE;
            break;
          case COMMAND_OFFSET:
            cmdReg.command = *data;
            action = ACT_CMD_WRITE;
            break;
          default:
            panic("Invalid IDE command register offset: %#x\n", offset);
        }
        break;
      case CONTROL_BLOCK:
        if (offset == CONTROL_OFFSET) {
            if (*data & CONTROL_RST_BIT) {
                // force the device into the reset state
                devState = Device_Srst;
                action = ACT_SRST_SET;
            } else if (devState == Device_Srst && !(*data & CONTROL_RST_BIT))
                action = ACT_SRST_CLEAR;

            nIENBit = (*data & CONTROL_IEN_BIT) ? true : false;
        }
        else
            panic("Invalid IDE control register offset: %#x\n", offset);
        break;
      default:
        panic("Unknown register block!\n");
    }

    if (action != ACT_NONE)
        updateState(action);
}

////
// Perform DMA transactions
////

void
IdeDisk::doDmaTransfer()
{
    if (dmaState != Dma_Transfer || devState != Transfer_Data_Dma)
        panic("Inconsistent DMA transfer state: dmaState = %d devState = %d\n",
              dmaState, devState);

    // first read the current PRD
    if (dmaInterface) {
        if (dmaInterface->busy()) {
            // reschedule after waiting period
            dmaTransferEvent.schedule(curTick + DMA_BACKOFF_PERIOD);
            return;
        }

        dmaInterface->doDMA(Read, curPrdAddr, sizeof(PrdEntry_t), curTick,
                            &dmaPrdReadEvent);
    } else {
        dmaPrdReadDone();
    }
}

void
IdeDisk::dmaPrdReadDone()
{
    // actually copy the PRD from physical memory
    memcpy((void *)&curPrd.entry,
           physmem->dma_addr(curPrdAddr, sizeof(PrdEntry_t)),
           sizeof(PrdEntry_t));

    DPRINTF(IdeDisk,
            "PRD: baseAddr:%#x (%#x) byteCount:%d (%d) eot:%#x sector:%d\n",
            curPrd.getBaseAddr(), pciToDma(curPrd.getBaseAddr()),
            curPrd.getByteCount(), (cmdBytesLeft/SectorSize),
            curPrd.getEOT(), curSector);

    // the prd pointer has already been translated, so just do an increment
    curPrdAddr = curPrdAddr + sizeof(PrdEntry_t);

    if (dmaRead)
        doDmaRead();
    else
        doDmaWrite();
}

void
IdeDisk::doDmaRead()
{
    /** @todo we need to figure out what the delay actually will be */
    Tick totalDiskDelay = diskDelay + (curPrd.getByteCount() / SectorSize);

    DPRINTF(IdeDisk, "doDmaRead, diskDelay: %d totalDiskDelay: %d\n",
            diskDelay, totalDiskDelay);
    if (dmaInterface) {
        if (dmaInterface->busy()) {
            // reschedule after waiting period
            dmaReadWaitEvent.schedule(curTick + DMA_BACKOFF_PERIOD);
            return;
        }

        Addr dmaAddr = pciToDma(curPrd.getBaseAddr());

        uint32_t bytesInPage = bytesInDmaPage(curPrd.getBaseAddr(),
                                              (uint32_t)curPrd.getByteCount());

        dmaInterfaceBytes = bytesInPage;

        dmaInterface->doDMA(Read, dmaAddr, bytesInPage,
                            curTick + totalDiskDelay, &dmaReadEvent);
    } else {
        // schedule dmaReadEvent with sectorDelay (dmaReadDone)
        dmaReadEvent.schedule(curTick + totalDiskDelay);
    }
}

void
IdeDisk::dmaReadDone()
{

    Addr curAddr = 0, dmaAddr = 0;
    uint32_t bytesWritten = 0, bytesInPage = 0, bytesLeft = 0;

    // continue to use the DMA interface until all pages are read
    if (dmaInterface && (dmaInterfaceBytes < curPrd.getByteCount())) {
        // see if the interface is busy
        if (dmaInterface->busy()) {
            // reschedule after waiting period
            dmaReadEvent.schedule(curTick + DMA_BACKOFF_PERIOD);
            return;
        }

        uint32_t bytesLeft = curPrd.getByteCount() - dmaInterfaceBytes;
        curAddr = curPrd.getBaseAddr() + dmaInterfaceBytes;
        dmaAddr = pciToDma(curAddr);

        bytesInPage = bytesInDmaPage(curAddr, bytesLeft);
        dmaInterfaceBytes += bytesInPage;

        dmaInterface->doDMA(Read, dmaAddr, bytesInPage,
                            curTick, &dmaReadEvent);

        return;
    }

    // set initial address
    curAddr = curPrd.getBaseAddr();

    // clear out the data buffer
    memset(dataBuffer, 0, MAX_DMA_SIZE);

    // read the data from memory via DMA into a data buffer
    while (bytesWritten < curPrd.getByteCount()) {
        if (cmdBytesLeft <= 0)
            panic("DMA data is larger than # of sectors specified\n");

        dmaAddr = pciToDma(curAddr);

        // calculate how many bytes are in the current page
        bytesLeft = curPrd.getByteCount() - bytesWritten;
        bytesInPage = bytesInDmaPage(curAddr, bytesLeft);

        // copy the data from memory into the data buffer
        memcpy((void *)(dataBuffer + bytesWritten),
               physmem->dma_addr(dmaAddr, bytesInPage),
               bytesInPage);

        curAddr += bytesInPage;
        bytesWritten += bytesInPage;
        cmdBytesLeft -= bytesInPage;
    }

    // write the data to the disk image
    for (bytesWritten = 0;
         bytesWritten < curPrd.getByteCount();
         bytesWritten += SectorSize) {

        writeDisk(curSector++, (uint8_t *)(dataBuffer + bytesWritten));
    }

    // check for the EOT
    if (curPrd.getEOT()) {
        assert(cmdBytesLeft == 0);
        dmaState = Dma_Idle;
        updateState(ACT_DMA_DONE);
    } else {
        doDmaTransfer();
    }
}

void
IdeDisk::doDmaWrite()
{
    /** @todo we need to figure out what the delay actually will be */
    Tick totalDiskDelay = diskDelay + (curPrd.getByteCount() / SectorSize);

    DPRINTF(IdeDisk, "doDmaWrite, diskDelay: %d totalDiskDelay: %d\n",
            diskDelay, totalDiskDelay);

    if (dmaInterface) {
        if (dmaInterface->busy()) {
            // reschedule after waiting period
            dmaWriteWaitEvent.schedule(curTick + DMA_BACKOFF_PERIOD);
            return;
        }

        Addr dmaAddr = pciToDma(curPrd.getBaseAddr());

        uint32_t bytesInPage = bytesInDmaPage(curPrd.getBaseAddr(),
                                              (uint32_t)curPrd.getByteCount());

        dmaInterfaceBytes = bytesInPage;

        dmaInterface->doDMA(WriteInvalidate, dmaAddr,
                            bytesInPage, curTick + totalDiskDelay,
                            &dmaWriteEvent);
    } else {
        // schedule event with disk delay (dmaWriteDone)
        dmaWriteEvent.schedule(curTick + totalDiskDelay);
    }
}

void
IdeDisk::dmaWriteDone()
{
    Addr curAddr = 0, pageAddr = 0, dmaAddr = 0;
    uint32_t bytesRead = 0, bytesInPage = 0;

    // continue to use the DMA interface until all pages are read
    if (dmaInterface && (dmaInterfaceBytes < curPrd.getByteCount())) {
        // see if the interface is busy
        if (dmaInterface->busy()) {
            // reschedule after waiting period
            dmaWriteEvent.schedule(curTick + DMA_BACKOFF_PERIOD);
            return;
        }

        uint32_t bytesLeft = curPrd.getByteCount() - dmaInterfaceBytes;
        curAddr = curPrd.getBaseAddr() + dmaInterfaceBytes;
        dmaAddr = pciToDma(curAddr);

        bytesInPage = bytesInDmaPage(curAddr, bytesLeft);
        dmaInterfaceBytes += bytesInPage;

        dmaInterface->doDMA(WriteInvalidate, dmaAddr,
                            bytesInPage, curTick,
                            &dmaWriteEvent);

        return;
    }

    // setup the initial page and DMA address
    curAddr = curPrd.getBaseAddr();
    pageAddr = TheISA::TruncPage(curAddr);
    dmaAddr = pciToDma(curAddr);

    // clear out the data buffer
    memset(dataBuffer, 0, MAX_DMA_SIZE);

    while (bytesRead < curPrd.getByteCount()) {
        // see if we have crossed into a new page
        if (pageAddr != TheISA::TruncPage(curAddr)) {
            // write the data to memory
            memcpy(physmem->dma_addr(dmaAddr, bytesInPage),
                   (void *)(dataBuffer + (bytesRead - bytesInPage)),
                   bytesInPage);

            // update the DMA address and page address
            pageAddr = TheISA::TruncPage(curAddr);
            dmaAddr = pciToDma(curAddr);

            bytesInPage = 0;
        }

        if (cmdBytesLeft <= 0)
            panic("DMA requested data is larger than # sectors specified\n");

        readDisk(curSector++, (uint8_t *)(dataBuffer + bytesRead));

        curAddr += SectorSize;
        bytesRead += SectorSize;
        bytesInPage += SectorSize;
        cmdBytesLeft -= SectorSize;
    }

    // write the last page worth read to memory
    if (bytesInPage != 0) {
        memcpy(physmem->dma_addr(dmaAddr, bytesInPage),
               (void *)(dataBuffer + (bytesRead - bytesInPage)),
               bytesInPage);
    }

    // check for the EOT
    if (curPrd.getEOT()) {
        assert(cmdBytesLeft == 0);
        dmaState = Dma_Idle;
        updateState(ACT_DMA_DONE);
    } else {
        doDmaTransfer();
    }
}

////
// Disk utility routines
///

void
IdeDisk::readDisk(uint32_t sector, uint8_t *data)
{
    uint32_t bytesRead = image->read(data, sector);

    if (bytesRead != SectorSize)
        panic("Can't read from %s. Only %d of %d read. errno=%d\n",
              name(), bytesRead, SectorSize, errno);
}

void
IdeDisk::writeDisk(uint32_t sector, uint8_t *data)
{
    uint32_t bytesWritten = image->write(data, sector);

    if (bytesWritten != SectorSize)
        panic("Can't write to %s. Only %d of %d written. errno=%d\n",
              name(), bytesWritten, SectorSize, errno);
}

////
// Setup and handle commands
////

void
IdeDisk::startDma(const uint32_t &prdTableBase)
{
    if (dmaState != Dma_Start)
        panic("Inconsistent DMA state, should be in Dma_Start!\n");

    if (devState != Transfer_Data_Dma)
        panic("Inconsistent device state for DMA start!\n");

    // PRD base address is given by bits 31:2
    curPrdAddr = pciToDma((Addr)(prdTableBase & ~ULL(0x3)));

    dmaState = Dma_Transfer;

    // schedule dma transfer (doDmaTransfer)
    dmaTransferEvent.schedule(curTick + 1);
}

void
IdeDisk::abortDma()
{
    if (dmaState == Dma_Idle)
        panic("Inconsistent DMA state, should be Start or Transfer!");

    if (devState != Transfer_Data_Dma && devState != Prepare_Data_Dma)
        panic("Inconsistent device state, should be Transfer or Prepare!\n");

    updateState(ACT_CMD_ERROR);
}

void
IdeDisk::startCommand()
{
    DevAction_t action = ACT_NONE;
    uint32_t size = 0;
    dmaRead = false;

    // Decode commands
    switch (cmdReg.command) {
        // Supported non-data commands
      case WDSF_READ_NATIVE_MAX:
        size = image->size() - 1;
        cmdReg.sec_num = (size & 0xff);
        cmdReg.cyl_low = ((size & 0xff00) >> 8);
        cmdReg.cyl_high = ((size & 0xff0000) >> 16);
        cmdReg.head = ((size & 0xf000000) >> 24);

        devState = Command_Execution;
        action = ACT_CMD_COMPLETE;
        break;

      case WDCC_RECAL:
      case WDCC_IDP:
      case WDCC_STANDBY_IMMED:
      case WDCC_FLUSHCACHE:
      case WDSF_VERIFY:
      case WDSF_SEEK:
      case SET_FEATURES:
      case WDCC_SETMULTI:
        devState = Command_Execution;
        action = ACT_CMD_COMPLETE;
        break;

        // Supported PIO data-in commands
      case WDCC_IDENTIFY:
        cmdBytes = cmdBytesLeft = sizeof(struct ataparams);
        devState = Prepare_Data_In;
        action = ACT_DATA_READY;
        break;

      case WDCC_READMULTI:
      case WDCC_READ:
        if (!(cmdReg.drive & DRIVE_LBA_BIT))
            panic("Attempt to perform CHS access, only supports LBA\n");

        if (cmdReg.sec_count == 0)
            cmdBytes = cmdBytesLeft = (256 * SectorSize);
        else
            cmdBytes = cmdBytesLeft = (cmdReg.sec_count * SectorSize);

        curSector = getLBABase();

        /** @todo make this a scheduled event to simulate disk delay */
        devState = Prepare_Data_In;
        action = ACT_DATA_READY;
        break;

        // Supported PIO data-out commands
      case WDCC_WRITEMULTI:
      case WDCC_WRITE:
        if (!(cmdReg.drive & DRIVE_LBA_BIT))
            panic("Attempt to perform CHS access, only supports LBA\n");

        if (cmdReg.sec_count == 0)
            cmdBytes = cmdBytesLeft = (256 * SectorSize);
        else
            cmdBytes = cmdBytesLeft = (cmdReg.sec_count * SectorSize);

        curSector = getLBABase();

        devState = Prepare_Data_Out;
        action = ACT_DATA_READY;
        break;

        // Supported DMA commands
      case WDCC_WRITEDMA:
        dmaRead = true;  // a write to the disk is a DMA read from memory
      case WDCC_READDMA:
        if (!(cmdReg.drive & DRIVE_LBA_BIT))
            panic("Attempt to perform CHS access, only supports LBA\n");

        if (cmdReg.sec_count == 0)
            cmdBytes = cmdBytesLeft = (256 * SectorSize);
        else
            cmdBytes = cmdBytesLeft = (cmdReg.sec_count * SectorSize);

        curSector = getLBABase();

        devState = Prepare_Data_Dma;
        action = ACT_DMA_READY;
        break;

      default:
        panic("Unsupported ATA command: %#x\n", cmdReg.command);
    }

    if (action != ACT_NONE) {
        // set the BSY bit
        status |= STATUS_BSY_BIT;
        // clear the DRQ bit
        status &= ~STATUS_DRQ_BIT;
        // clear the DF bit
        status &= ~STATUS_DF_BIT;

        updateState(action);
    }
}

////
// Handle setting and clearing interrupts
////

void
IdeDisk::intrPost()
{
    DPRINTF(IdeDisk, "Posting Interrupt\n");
    if (intrPending)
        panic("Attempt to post an interrupt with one pending\n");

    intrPending = true;

    // talk to controller to set interrupt
    if (ctrl) {
        ctrl->bmi_regs.bmis0 |= IDEINTS;
        ctrl->intrPost();
    }
}

void
IdeDisk::intrClear()
{
    DPRINTF(IdeDisk, "Clearing Interrupt\n");
    if (!intrPending)
        panic("Attempt to clear a non-pending interrupt\n");

    intrPending = false;

    // talk to controller to clear interrupt
    if (ctrl)
        ctrl->intrClear();
}

////
// Manage the device internal state machine
////

void
IdeDisk::updateState(DevAction_t action)
{
    switch (devState) {
      case Device_Srst:
        if (action == ACT_SRST_SET) {
            // set the BSY bit
            status |= STATUS_BSY_BIT;
        } else if (action == ACT_SRST_CLEAR) {
            // clear the BSY bit
            status &= ~STATUS_BSY_BIT;

            // reset the device state
            reset(devID);
        }
        break;

      case Device_Idle_S:
        if (action == ACT_SELECT_WRITE && !isDEVSelect()) {
            devState = Device_Idle_NS;
        } else if (action == ACT_CMD_WRITE) {
            startCommand();
        }

        break;

      case Device_Idle_SI:
        if (action == ACT_SELECT_WRITE && !isDEVSelect()) {
            devState = Device_Idle_NS;
            intrClear();
        } else if (action == ACT_STAT_READ || isIENSet()) {
            devState = Device_Idle_S;
            intrClear();
        } else if (action == ACT_CMD_WRITE) {
            intrClear();
            startCommand();
        }

        break;

      case Device_Idle_NS:
        if (action == ACT_SELECT_WRITE && isDEVSelect()) {
            if (!isIENSet() && intrPending) {
                devState = Device_Idle_SI;
                intrPost();
            }
            if (isIENSet() || !intrPending) {
                devState = Device_Idle_S;
            }
        }
        break;

      case Command_Execution:
        if (action == ACT_CMD_COMPLETE) {
            // clear the BSY bit
            setComplete();

            if (!isIENSet()) {
                devState = Device_Idle_SI;
                intrPost();
            } else {
                devState = Device_Idle_S;
            }
        }
        break;

      case Prepare_Data_In:
        if (action == ACT_CMD_ERROR) {
            // clear the BSY bit
            setComplete();

            if (!isIENSet()) {
                devState = Device_Idle_SI;
                intrPost();
            } else {
                devState = Device_Idle_S;
            }
        } else if (action == ACT_DATA_READY) {
            // clear the BSY bit
            status &= ~STATUS_BSY_BIT;
            // set the DRQ bit
            status |= STATUS_DRQ_BIT;

            // copy the data into the data buffer
            if (cmdReg.command == WDCC_IDENTIFY) {
                // Reset the drqBytes for this block
                drqBytesLeft = sizeof(struct ataparams);

                memcpy((void *)dataBuffer, (void *)&driveID,
                       sizeof(struct ataparams));
            } else {
                // Reset the drqBytes for this block
                drqBytesLeft = SectorSize;

                readDisk(curSector++, dataBuffer);
            }

            // put the first two bytes into the data register
            memcpy((void *)&cmdReg.data, (void *)dataBuffer,
                   sizeof(uint16_t));

            if (!isIENSet()) {
                devState = Data_Ready_INTRQ_In;
                intrPost();
            } else {
                devState = Transfer_Data_In;
            }
        }
        break;

      case Data_Ready_INTRQ_In:
        if (action == ACT_STAT_READ) {
            devState = Transfer_Data_In;
            intrClear();
        }
        break;

      case Transfer_Data_In:
        if (action == ACT_DATA_READ_BYTE || action == ACT_DATA_READ_SHORT) {
            if (action == ACT_DATA_READ_BYTE) {
                panic("DEBUG: READING DATA ONE BYTE AT A TIME!\n");
            } else {
                drqBytesLeft -= 2;
                cmdBytesLeft -= 2;

                // copy next short into data registers
                if (drqBytesLeft)
                    memcpy((void *)&cmdReg.data,
                           (void *)&dataBuffer[SectorSize - drqBytesLeft],
                           sizeof(uint16_t));
            }

            if (drqBytesLeft == 0) {
                if (cmdBytesLeft == 0) {
                    // Clear the BSY bit
                    setComplete();
                    devState = Device_Idle_S;
                } else {
                    devState = Prepare_Data_In;
                    // set the BSY_BIT
                    status |= STATUS_BSY_BIT;
                    // clear the DRQ_BIT
                    status &= ~STATUS_DRQ_BIT;

                    /** @todo change this to a scheduled event to simulate
                        disk delay */
                    updateState(ACT_DATA_READY);
                }
            }
        }
        break;

      case Prepare_Data_Out:
        if (action == ACT_CMD_ERROR || cmdBytesLeft == 0) {
            // clear the BSY bit
            setComplete();

            if (!isIENSet()) {
                devState = Device_Idle_SI;
                intrPost();
            } else {
                devState = Device_Idle_S;
            }
        } else if (action == ACT_DATA_READY && cmdBytesLeft != 0) {
            // clear the BSY bit
            status &= ~STATUS_BSY_BIT;
            // set the DRQ bit
            status |= STATUS_DRQ_BIT;

            // clear the data buffer to get it ready for writes
            memset(dataBuffer, 0, MAX_DMA_SIZE);

            // reset the drqBytes for this block
            drqBytesLeft = SectorSize;

            if (cmdBytesLeft == cmdBytes || isIENSet()) {
                devState = Transfer_Data_Out;
            } else {
                devState = Data_Ready_INTRQ_Out;
                intrPost();
            }
        }
        break;

      case Data_Ready_INTRQ_Out:
        if (action == ACT_STAT_READ) {
            devState = Transfer_Data_Out;
            intrClear();
        }
        break;

      case Transfer_Data_Out:
        if (action == ACT_DATA_WRITE_BYTE ||
            action == ACT_DATA_WRITE_SHORT) {

            if (action == ACT_DATA_READ_BYTE) {
                panic("DEBUG: WRITING DATA ONE BYTE AT A TIME!\n");
            } else {
                // copy the latest short into the data buffer
                memcpy((void *)&dataBuffer[SectorSize - drqBytesLeft],
                       (void *)&cmdReg.data,
                       sizeof(uint16_t));

                drqBytesLeft -= 2;
                cmdBytesLeft -= 2;
            }

            if (drqBytesLeft == 0) {
                // copy the block to the disk
                writeDisk(curSector++, dataBuffer);

                // set the BSY bit
                status |= STATUS_BSY_BIT;
                // set the seek bit
                status |= STATUS_SEEK_BIT;
                // clear the DRQ bit
                status &= ~STATUS_DRQ_BIT;

                devState = Prepare_Data_Out;

                /** @todo change this to a scheduled event to simulate
                    disk delay */
                updateState(ACT_DATA_READY);
            }
        }
        break;

      case Prepare_Data_Dma:
        if (action == ACT_CMD_ERROR) {
            // clear the BSY bit
            setComplete();

            if (!isIENSet()) {
                devState = Device_Idle_SI;
                intrPost();
            } else {
                devState = Device_Idle_S;
            }
        } else if (action == ACT_DMA_READY) {
            // clear the BSY bit
            status &= ~STATUS_BSY_BIT;
            // set the DRQ bit
            status |= STATUS_DRQ_BIT;

            devState = Transfer_Data_Dma;

            if (dmaState != Dma_Idle)
                panic("Inconsistent DMA state, should be Dma_Idle\n");

            dmaState = Dma_Start;
            // wait for the write to the DMA start bit
        }
        break;

      case Transfer_Data_Dma:
        if (action == ACT_CMD_ERROR || action == ACT_DMA_DONE) {
            // clear the BSY bit
            setComplete();
            // set the seek bit
            status |= STATUS_SEEK_BIT;
            // clear the controller state for DMA transfer
            ctrl->setDmaComplete(this);

            if (!isIENSet()) {
                devState = Device_Idle_SI;
                intrPost();
            } else {
                devState = Device_Idle_S;
            }
        }
        break;

      default:
        panic("Unknown IDE device state: %#x\n", devState);
    }
}

void
IdeDisk::serialize(ostream &os)
{
    // Check all outstanding events to see if they are scheduled
    // these are all mutually exclusive
    Tick reschedule = 0;
    Events_t event = None;

    int eventCount = 0;

    if (dmaTransferEvent.scheduled()) {
        reschedule = dmaTransferEvent.when();
        event = Transfer;
        eventCount++;
    }
    if (dmaReadWaitEvent.scheduled()) {
        reschedule = dmaReadWaitEvent.when();
        event = ReadWait;
        eventCount++;
    }
    if (dmaWriteWaitEvent.scheduled()) {
        reschedule = dmaWriteWaitEvent.when();
        event = WriteWait;
        eventCount++;
    }
    if (dmaPrdReadEvent.scheduled()) {
        reschedule = dmaPrdReadEvent.when();
        event = PrdRead;
        eventCount++;
    }
    if (dmaReadEvent.scheduled()) {
        reschedule = dmaReadEvent.when();
        event = DmaRead;
        eventCount++;
    }
    if (dmaWriteEvent.scheduled()) {
        reschedule = dmaWriteEvent.when();
        event = DmaWrite;
        eventCount++;
    }

    assert(eventCount <= 1);

    SERIALIZE_SCALAR(reschedule);
    SERIALIZE_ENUM(event);

    // Serialize device registers
    SERIALIZE_SCALAR(cmdReg.data);
    SERIALIZE_SCALAR(cmdReg.sec_count);
    SERIALIZE_SCALAR(cmdReg.sec_num);
    SERIALIZE_SCALAR(cmdReg.cyl_low);
    SERIALIZE_SCALAR(cmdReg.cyl_high);
    SERIALIZE_SCALAR(cmdReg.drive);
    SERIALIZE_SCALAR(cmdReg.command);
    SERIALIZE_SCALAR(status);
    SERIALIZE_SCALAR(nIENBit);
    SERIALIZE_SCALAR(devID);

    // Serialize the PRD related information
    SERIALIZE_SCALAR(curPrd.entry.baseAddr);
    SERIALIZE_SCALAR(curPrd.entry.byteCount);
    SERIALIZE_SCALAR(curPrd.entry.endOfTable);
    SERIALIZE_SCALAR(curPrdAddr);

    // Serialize current transfer related information
    SERIALIZE_SCALAR(cmdBytesLeft);
    SERIALIZE_SCALAR(cmdBytes);
    SERIALIZE_SCALAR(drqBytesLeft);
    SERIALIZE_SCALAR(curSector);
    SERIALIZE_SCALAR(dmaRead);
    SERIALIZE_SCALAR(dmaInterfaceBytes);
    SERIALIZE_SCALAR(intrPending);
    SERIALIZE_ENUM(devState);
    SERIALIZE_ENUM(dmaState);
    SERIALIZE_ARRAY(dataBuffer, MAX_DMA_SIZE);
}

void
IdeDisk::unserialize(Checkpoint *cp, const string &section)
{
    // Reschedule events that were outstanding
    // these are all mutually exclusive
    Tick reschedule = 0;
    Events_t event = None;

    UNSERIALIZE_SCALAR(reschedule);
    UNSERIALIZE_ENUM(event);

    switch (event) {
      case None : break;
      case Transfer : dmaTransferEvent.schedule(reschedule); break;
      case ReadWait : dmaReadWaitEvent.schedule(reschedule); break;
      case WriteWait : dmaWriteWaitEvent.schedule(reschedule); break;
      case PrdRead : dmaPrdReadEvent.schedule(reschedule); break;
      case DmaRead : dmaReadEvent.schedule(reschedule); break;
      case DmaWrite : dmaWriteEvent.schedule(reschedule); break;
    }

    // Unserialize device registers
    UNSERIALIZE_SCALAR(cmdReg.data);
    UNSERIALIZE_SCALAR(cmdReg.sec_count);
    UNSERIALIZE_SCALAR(cmdReg.sec_num);
    UNSERIALIZE_SCALAR(cmdReg.cyl_low);
    UNSERIALIZE_SCALAR(cmdReg.cyl_high);
    UNSERIALIZE_SCALAR(cmdReg.drive);
    UNSERIALIZE_SCALAR(cmdReg.command);
    UNSERIALIZE_SCALAR(status);
    UNSERIALIZE_SCALAR(nIENBit);
    UNSERIALIZE_SCALAR(devID);

    // Unserialize the PRD related information
    UNSERIALIZE_SCALAR(curPrd.entry.baseAddr);
    UNSERIALIZE_SCALAR(curPrd.entry.byteCount);
    UNSERIALIZE_SCALAR(curPrd.entry.endOfTable);
    UNSERIALIZE_SCALAR(curPrdAddr);

    // Unserialize current transfer related information
    UNSERIALIZE_SCALAR(cmdBytes);
    UNSERIALIZE_SCALAR(cmdBytesLeft);
    UNSERIALIZE_SCALAR(drqBytesLeft);
    UNSERIALIZE_SCALAR(curSector);
    UNSERIALIZE_SCALAR(dmaRead);
    UNSERIALIZE_SCALAR(dmaInterfaceBytes);
    UNSERIALIZE_SCALAR(intrPending);
    UNSERIALIZE_ENUM(devState);
    UNSERIALIZE_ENUM(dmaState);
    UNSERIALIZE_ARRAY(dataBuffer, MAX_DMA_SIZE);
}

#ifndef DOXYGEN_SHOULD_SKIP_THIS

enum DriveID { master, slave };
static const char *DriveID_strings[] = { "master", "slave" };
BEGIN_DECLARE_SIM_OBJECT_PARAMS(IdeDisk)

    SimObjectParam<DiskImage *> image;
    SimObjectParam<PhysicalMemory *> physmem;
    SimpleEnumParam<DriveID> driveID;
    Param<int> delay;

END_DECLARE_SIM_OBJECT_PARAMS(IdeDisk)

BEGIN_INIT_SIM_OBJECT_PARAMS(IdeDisk)

    INIT_PARAM(image, "Disk image"),
    INIT_PARAM(physmem, "Physical memory"),
    INIT_ENUM_PARAM(driveID, "Drive ID (0=master 1=slave)", DriveID_strings),
    INIT_PARAM_DFLT(delay, "Fixed disk delay in microseconds", 1)

END_INIT_SIM_OBJECT_PARAMS(IdeDisk)


CREATE_SIM_OBJECT(IdeDisk)
{
    return new IdeDisk(getInstanceName(), image, physmem, driveID, delay);
}

REGISTER_SIM_OBJECT("IdeDisk", IdeDisk)

#endif //DOXYGEN_SHOULD_SKIP_THIS