/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * ASPI I/O functions.
 *
 * Some notes on ASPI stuff:
 *  - The Nero ASPI provides an interface for IDE hard drives.  It also
 *    throws in a couple of mystery devices on a host adapter at the end.
 *    It has "unknown" device type and doesn't respond to SCSI device
 *    inquiries, so it's easy to ignore.
 *  - The Win98 generic ASPI only finds CD-ROM drives on the IDE bus.
 */
#include "StdAfx.h"
#if defined(_WIN32) && defined (WANT_ASPI)

#include "DiskImgPriv.h"
#include "SCSIDefs.h"
#include "CP_wnaspi32.h"
#include "ASPI.h"


/*
 * Initialize ASPI.
 */
DIError ASPI::Init(void)
{
    DWORD aspiStatus;
    static const char* kASPIDllName = "wnaspi32.dll";

    /*
     * Try to load the DLL.
     */
    fhASPI = ::LoadLibrary(kASPIDllName);
    if (fhASPI == NULL) {
        DWORD lastErr = ::GetLastError();
        if (lastErr == ERROR_MOD_NOT_FOUND) {
            LOGI("ASPI DLL '%s' not found", kASPIDllName);
        } else {
            LOGI("ASPI LoadLibrary(%s) failed (err=%ld)",
                kASPIDllName, GetLastError());
        }
        return kDIErrGeneric;
    }

    GetASPI32SupportInfo = (DWORD(*)(void))::GetProcAddress(fhASPI, "GetASPI32SupportInfo");
    SendASPI32Command = (DWORD(*)(LPSRB))::GetProcAddress(fhASPI, "SendASPI32Command");
    GetASPI32DLLVersion = (DWORD(*)(void))::GetProcAddress(fhASPI, "GetASPI32DLLVersion");
    if (GetASPI32SupportInfo == NULL || SendASPI32Command == NULL) {
        LOGI("ASPI functions not found in dll");
        ::FreeLibrary(fhASPI);
        fhASPI = NULL;
        return kDIErrGeneric;
    }

    if (GetASPI32DLLVersion != NULL) {
        fASPIVersion = GetASPI32DLLVersion();
        LOGI(" ASPI version is %d.%d.%d.%d",
            fASPIVersion & 0x0ff,
            (fASPIVersion >> 8) & 0xff,
            (fASPIVersion >> 16) & 0xff,
            (fASPIVersion >> 24) & 0xff);
    } else {
        LOGI("ASPI WARNING: couldn't find GetASPI32DLLVersion interface");
    }

    /*
     * Successfully loaded the library.  Start it up and see if it works.
     */
    aspiStatus = GetASPI32SupportInfo();
    if (HIBYTE(LOWORD(aspiStatus)) != SS_COMP) {
        LOGI("ASPI loaded but not working (status=%d)",
            HIBYTE(LOWORD(aspiStatus)));
        ::FreeLibrary(fhASPI);
        fhASPI = NULL;
        return kDIErrASPIFailure;
    }

    fHostAdapterCount = LOBYTE(LOWORD(aspiStatus));
    LOGI("ASPI loaded successfully, hostAdapterCount=%d",
        fHostAdapterCount);

    return kDIErrNone;
}

/*
 * Destructor.  Unload the ASPI DLL.
 */
ASPI::~ASPI(void)
{
    if (fhASPI != NULL) {
        LOGI("Unloading ASPI DLL");
        ::FreeLibrary(fhASPI);
        fhASPI = NULL;
    }
}


/*
 * Issue an ASPI host adapter inquiry request for the specified adapter.
 *
 * Pass in a pointer to a struct that receives the result.
 */
DIError ASPI::HostAdapterInquiry(unsigned char adapter, AdapterInfo* pAdapterInfo)
{
    SRB_HAInquiry req;
    DWORD result;

    assert(adapter >= 0 && adapter < kMaxAdapters);

    memset(&req, 0, sizeof(req));
    req.SRB_Cmd = SC_HA_INQUIRY;
    req.SRB_HaId = adapter;

    result = SendASPI32Command(&req);
    if (result != SS_COMP) {
        LOGI("ASPI(SC_HA_INQUIRY on %d) failed with result=0x%lx",
            adapter, result);
        return kDIErrASPIFailure;
    }

    pAdapterInfo->adapterScsiID = req.HA_SCSI_ID;
    memcpy(pAdapterInfo->managerID, req.HA_ManagerId,
        sizeof(pAdapterInfo->managerID)-1);
    pAdapterInfo->managerID[sizeof(pAdapterInfo->managerID)-1] = '\0';
    memcpy(pAdapterInfo->identifier, req.HA_Identifier,
        sizeof(pAdapterInfo->identifier)-1);
    pAdapterInfo->identifier[sizeof(pAdapterInfo->identifier)-1] = '\0';
    pAdapterInfo->maxTargets = req.HA_Unique[3];
    pAdapterInfo->bufferAlignment =
        (unsigned short) req.HA_Unique[1] << 8 | req.HA_Unique[0];

    return kDIErrNone;
}

/*
 * Issue an ASPI query on device type.
 */
DIError ASPI::GetDeviceType(unsigned char adapter, unsigned char target,
    unsigned char lun, unsigned char* pType)
{
    SRB_GDEVBlock req;
    DWORD result;

    assert(adapter >= 0 && adapter < kMaxAdapters);
    assert(target >= 0 && target < kMaxTargets);
    assert(lun >= 0 && lun < kMaxLuns);
    assert(pType != NULL);

    memset(&req, 0, sizeof(req));
    req.SRB_Cmd = SC_GET_DEV_TYPE;
    req.SRB_HaId = adapter;
    req.SRB_Target = target;
    req.SRB_Lun = lun;

    result = SendASPI32Command(&req);
    if (result != SS_COMP)
        return kDIErrASPIFailure;

    *pType = req.SRB_DeviceType;

    return kDIErrNone;
}

/*
 * Return a printable string for the given device type.
 */
const char* ASPI::DeviceTypeToString(unsigned char deviceType)
{
    switch (deviceType) {
    case kScsiDevTypeDASD:      return "Disk device";
    case kScsiDevTypeSEQD:      return "Tape device";
    case kScsiDevTypePRNT:      return "Printer";
    case kScsiDevTypePROC:      return "Processor";
    case kScsiDevTypeWORM:      return "Write-once read-multiple";
    case kScsiDevTypeCDROM:     return "CD-ROM device";
    case kScsiDevTypeSCAN:      return "Scanner device";
    case kScsiDevTypeOPTI:      return "Optical memory device";
    case kScsiDevTypeJUKE:      return "Medium changer device";
    case kScsiDevTypeCOMM:      return "Communications device";
    case kScsiDevTypeUNKNOWN:   return "Unknown or no device type";
    default:            return "Invalid type";
    }
}

/*
 * Issue a SCSI device inquiry and return the interesting parts.
 */
DIError ASPI::DeviceInquiry(unsigned char adapter, unsigned char target,
    unsigned char lun, Inquiry* pInquiry)
{
    DIError dierr;
    SRB_ExecSCSICmd srb;
    CDB6Inquiry* pCDB;
    unsigned char buf[96];  // enough to hold everything of interest, and more
    CDB_InquiryData* pInqData = (CDB_InquiryData*) buf;

    assert(sizeof(CDB6Inquiry) == 6);

    memset(&srb, 0, sizeof(srb));
    srb.SRB_Cmd = SC_EXEC_SCSI_CMD;
    srb.SRB_HaId = adapter;
    srb.SRB_Target = target;
    srb.SRB_Lun = lun;
    srb.SRB_Flags = SRB_DIR_IN;
    srb.SRB_BufLen = sizeof(buf);
    srb.SRB_BufPointer = buf;
    srb.SRB_SenseLen = SENSE_LEN;
    srb.SRB_CDBLen = sizeof(*pCDB);

    pCDB = (CDB6Inquiry*) srb.CDBByte;
    pCDB->operationCode = kScsiOpInquiry;
    pCDB->allocationLength = sizeof(buf);

    // Don't set pCDB->logicalUnitNumber.  It's only there for SCSI-1
    //  devices.  SCSI-2 uses an IDENTIFY command; I gather ASPI is doing
    //  this for us.

    dierr = ExecSCSICommand(&srb);
    if (dierr != kDIErrNone)
        return dierr;

    memcpy(pInquiry->vendorID, pInqData->vendorId,
        sizeof(pInquiry->vendorID)-1);
    pInquiry->vendorID[sizeof(pInquiry->vendorID)-1] = '\0';
    memcpy(pInquiry->productID, pInqData->productId,
        sizeof(pInquiry->productID)-1);
    pInquiry->productID[sizeof(pInquiry->productID)-1] = '\0';
    pInquiry->productRevision[0] = pInqData->productRevisionLevel[0];
    pInquiry->productRevision[1] = pInqData->productRevisionLevel[1];
    pInquiry->productRevision[2] = pInqData->productRevisionLevel[2];
    pInquiry->productRevision[3] = pInqData->productRevisionLevel[3];

    return kDIErrNone;
}


/*
 * Get the capacity of a SCSI block device.
 */
DIError ASPI::GetDeviceCapacity(unsigned char adapter, unsigned char target,
    unsigned char lun, unsigned long* pLastBlock, unsigned long* pBlockSize)
{
    DIError dierr;
    SRB_ExecSCSICmd srb;
    CDB10* pCDB;
    CDB_ReadCapacityData dataBuf;

    assert(sizeof(dataBuf) == 8);   // READ CAPACITY returns two longs
    assert(sizeof(CDB10) == 10);

    memset(&srb, 0, sizeof(srb));
    srb.SRB_Cmd = SC_EXEC_SCSI_CMD;
    srb.SRB_HaId = adapter;
    srb.SRB_Target = target;
    srb.SRB_Lun = lun;
    srb.SRB_Flags = SRB_DIR_IN;
    srb.SRB_BufLen = sizeof(dataBuf);
    srb.SRB_BufPointer = (unsigned char*)&dataBuf;
    srb.SRB_SenseLen = SENSE_LEN;
    srb.SRB_CDBLen = sizeof(*pCDB);

    pCDB = (CDB10*) srb.CDBByte;
    pCDB->operationCode = kScsiOpReadCapacity;
    // rest of CDB is zero

    dierr = ExecSCSICommand(&srb);
    if (dierr != kDIErrNone)
        return dierr;

    *pLastBlock =
            (unsigned long) dataBuf.logicalBlockAddr0 << 24 |
            (unsigned long) dataBuf.logicalBlockAddr1 << 16 |
            (unsigned long) dataBuf.logicalBlockAddr2 << 8 |
            (unsigned long) dataBuf.logicalBlockAddr3;
    *pBlockSize =
            (unsigned long) dataBuf.bytesPerBlock0 << 24 |
            (unsigned long) dataBuf.bytesPerBlock1 << 16 |
            (unsigned long) dataBuf.bytesPerBlock2 << 8 |
            (unsigned long) dataBuf.bytesPerBlock3;
    return kDIErrNone;
}

/*
 * Test to see if a device is ready.
 *
 * Returns "true" if the device is ready, "false" if not.
 */
DIError ASPI::TestUnitReady(unsigned char adapter, unsigned char target,
    unsigned char lun, bool* pReady)
{
    DIError dierr;
    SRB_ExecSCSICmd srb;
    CDB6* pCDB;

    assert(sizeof(CDB6) == 6);

    memset(&srb, 0, sizeof(srb));
    srb.SRB_Cmd = SC_EXEC_SCSI_CMD;
    srb.SRB_HaId = adapter;
    srb.SRB_Target = target;
    srb.SRB_Lun = lun;
    srb.SRB_Flags = 0; //SRB_DIR_IN;
    srb.SRB_BufLen = 0;
    srb.SRB_BufPointer = NULL;
    srb.SRB_SenseLen = SENSE_LEN;
    srb.SRB_CDBLen = sizeof(*pCDB);

    pCDB = (CDB6*) srb.CDBByte;
    pCDB->operationCode = kScsiOpTestUnitReady;
    // rest of CDB is zero

    dierr = ExecSCSICommand(&srb);
    if (dierr != kDIErrNone) {
        const CDB_SenseData* pSense = (const CDB_SenseData*) srb.SenseArea;

        if (srb.SRB_TargStat == kScsiStatCheckCondition &&
            pSense->senseKey == kScsiSenseNotReady)
        {
            // expect pSense->additionalSenseCode to be
            //  kScsiAdSenseNoMediaInDevice; no need to check it really.
            LOGI(" ASPI TestUnitReady: drive %d:%d:%d is NOT ready",
                adapter, target, lun);
        } else {
            LOGI(" ASPI TestUnitReady failed, status=0x%02x sense=0x%02x ASC=0x%02x",
                srb.SRB_TargStat, pSense->senseKey,
                pSense->additionalSenseCode);
        }
        *pReady = false;
    } else {
        const CDB_SenseData* pSense = (const CDB_SenseData*) srb.SenseArea;
        LOGI(" ASPI TestUnitReady: drive %d:%d:%d is ready",
            adapter, target, lun);
        //LOGI("   status=0x%02x sense=0x%02x ASC=0x%02x",
        //  srb.SRB_TargStat, pSense->senseKey, pSense->additionalSenseCode);
        *pReady = true;
    }

    return kDIErrNone;
}

/*
 * Read one or more blocks from the device.
 *
 * The block size is going to be whatever the device's native size is
 * (possibly modified by extents, but we'll ignore that).  For a CD-ROM
 * this means 2048-byte blocks.
 */
DIError ASPI::ReadBlocks(unsigned char adapter, unsigned char target,
    unsigned char lun, long startBlock, short numBlocks, long blockSize,
    void* buf)
{
    SRB_ExecSCSICmd srb;
    CDB10* pCDB;

    //LOGI(" ASPI ReadBlocks start=%ld num=%d (size=%d)",
    //  startBlock, numBlocks, blockSize);

    assert(sizeof(CDB10) == 10);
    assert(startBlock >= 0);
    assert(numBlocks > 0);
    assert(buf != NULL);

    memset(&srb, 0, sizeof(srb));
    srb.SRB_Cmd = SC_EXEC_SCSI_CMD;
    srb.SRB_HaId = adapter;
    srb.SRB_Target = target;
    srb.SRB_Lun = lun;
    srb.SRB_Flags = SRB_DIR_IN;
    srb.SRB_BufLen = numBlocks * blockSize;
    srb.SRB_BufPointer = (unsigned char*)buf;
    srb.SRB_SenseLen = SENSE_LEN;
    srb.SRB_CDBLen = sizeof(*pCDB);

    pCDB = (CDB10*) srb.CDBByte;
    pCDB->operationCode = kScsiOpRead;
    pCDB->logicalBlockAddr0 = (unsigned char) (startBlock >> 24);   // MSB
    pCDB->logicalBlockAddr1 = (unsigned char) (startBlock >> 16);
    pCDB->logicalBlockAddr2 = (unsigned char) (startBlock >> 8);
    pCDB->logicalBlockAddr3 = (unsigned char) startBlock;           // LSB
    pCDB->transferLength0 = (unsigned char) (numBlocks >> 8);       // MSB
    pCDB->transferLength1 = (unsigned char) numBlocks;              // LSB

    return ExecSCSICommand(&srb);
}

/*
 * Write one or more blocks to the device.
 */
DIError ASPI::WriteBlocks(unsigned char adapter, unsigned char target,
    unsigned char lun, long startBlock, short numBlocks, long blockSize,
    const void* buf)
{
    SRB_ExecSCSICmd srb;
    CDB10* pCDB;

    LOGI(" ASPI WriteBlocks start=%ld num=%d (size=%d)",
        startBlock, numBlocks, blockSize);

    assert(sizeof(CDB10) == 10);
    assert(startBlock >= 0);
    assert(numBlocks > 0);
    assert(buf != NULL);

    memset(&srb, 0, sizeof(srb));
    srb.SRB_Cmd = SC_EXEC_SCSI_CMD;
    srb.SRB_HaId = adapter;
    srb.SRB_Target = target;
    srb.SRB_Lun = lun;
    srb.SRB_Flags = SRB_DIR_IN;
    srb.SRB_BufLen = numBlocks * blockSize;
    srb.SRB_BufPointer = (unsigned char*)buf;
    srb.SRB_SenseLen = SENSE_LEN;
    srb.SRB_CDBLen = sizeof(*pCDB);

    pCDB = (CDB10*) srb.CDBByte;
    pCDB->operationCode = kScsiOpWrite;
    pCDB->logicalBlockAddr0 = (unsigned char) (startBlock >> 24);   // MSB
    pCDB->logicalBlockAddr1 = (unsigned char) (startBlock >> 16);
    pCDB->logicalBlockAddr2 = (unsigned char) (startBlock >> 8);
    pCDB->logicalBlockAddr3 = (unsigned char) startBlock;           // LSB
    pCDB->transferLength0 = (unsigned char) (numBlocks >> 8);       // MSB
    pCDB->transferLength1 = (unsigned char) numBlocks;              // LSB

    return ExecSCSICommand(&srb);
}


/*
 * Execute a SCSI command.
 *
 * Returns an error if ASPI reports an error or the SCSI status isn't
 * kScsiStatGood.
 *
 * The Nero ASPI layer typically returns immediately, and hands back an
 * SS_ERR when something fails.  Win98 ASPI does the SS_PENDING thang.
 */
DIError ASPI::ExecSCSICommand(SRB_ExecSCSICmd* pSRB)
{
    HANDLE completionEvent = NULL;
    DWORD eventStatus;
    DWORD aspiStatus;

    assert(pSRB->SRB_Cmd == SC_EXEC_SCSI_CMD);
    assert(pSRB->SRB_Flags == SRB_DIR_IN ||
           pSRB->SRB_Flags == SRB_DIR_OUT ||
           pSRB->SRB_Flags == 0);

    /*
     * Set up event-waiting stuff, as described in the Adaptec ASPI docs.
     */
    pSRB->SRB_Flags |= SRB_EVENT_NOTIFY;

    completionEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
    if (completionEvent == NULL) {
        LOGI("Failed creating a completion event?");
        return kDIErrGeneric;
    }

    pSRB->SRB_PostProc = completionEvent;

    /*
     * Send the request.
     */
    (void)SendASPI32Command((LPSRB) pSRB);
    aspiStatus = pSRB->SRB_Status;
    if (aspiStatus == SS_PENDING) {
        //LOGI("  (waiting for completion)");
        eventStatus = ::WaitForSingleObject(completionEvent, kTimeout * 1000);

        ::CloseHandle(completionEvent);

        if (eventStatus == WAIT_TIMEOUT) {
            LOGI("  ASPI exec timed out!");
            return kDIErrSCSIFailure;
        } else if (eventStatus != WAIT_OBJECT_0) {
            LOGI("  ASPI exec returned weird wait state %ld", eventStatus);
            return kDIErrGeneric;
        }
    }

    /*
     * Check the final status.
     */
    aspiStatus = pSRB->SRB_Status;

    if (aspiStatus == SS_COMP) {
        /* success! */
    } else if (aspiStatus == SS_ERR) {
        const CDB_SenseData* pSense = (const CDB_SenseData*) pSRB->SenseArea;

        LOGI("  ASPI SCSI command 0x%02x failed: scsiStatus=0x%02x"
              " senseKey=0x%02x ASC=0x%02x\n",
            pSRB->CDBByte[0], pSRB->SRB_TargStat,
            pSense->senseKey, pSense->additionalSenseCode);
        return kDIErrSCSIFailure;
    } else {
        // SS_ABORTED, SS_ABORT_FAIL, SS_NO_DEVICE, ...
        LOGI("  ASPI failed on command 0x%02x: aspiStatus=%d scsiStatus=%d",
            pSRB->CDBByte[0], aspiStatus, pSRB->SRB_TargStat);
        return kDIErrASPIFailure;
    }

    return kDIErrNone;
}


/*
 * Return an array of accessible devices we found.
 *
 * Only return the devices matching device types in "deviceMask".
 */
DIError ASPI::GetAccessibleDevices(int deviceMask, ASPIDevice** ppDeviceArray,
    int* pNumDevices)
{
    DIError dierr;
    ASPIDevice* deviceArray = NULL;
    int idx = 0;

    assert(deviceMask != 0);
    assert((deviceMask & ~(kDevMaskCDROM | kDevMaskHardDrive)) == 0);
    assert(ppDeviceArray != NULL);
    assert(pNumDevices != NULL);

    deviceArray = new ASPIDevice[kMaxAccessibleDrives];
    if (deviceArray == NULL)
        return kDIErrMalloc;

    LOGI("ASPI scanning %d host adapters", fHostAdapterCount);

    for (int ha = 0; ha < fHostAdapterCount; ha++) {
        AdapterInfo adi;

        dierr = HostAdapterInquiry(ha, &adi);
        if (dierr != kDIErrNone) {
            LOGI("  ASPI inquiry on %d failed", ha);
            continue;
        }

        LOGI(" ASPI host adapter %d (SCSI ID=%d)", ha, adi.adapterScsiID);
        LOGI("   identifier='%s' managerID='%s'",
            adi.identifier, adi.managerID);
        LOGI("   maxTargets=%d bufferAlignment=%d",
            adi.maxTargets, adi.bufferAlignment);

        int maxTargets = adi.maxTargets;
        if (!maxTargets) {
            /* Win98 ASPI reports zero here for ATAPI */
            maxTargets = 8;
        }
        if (maxTargets > kMaxTargets)
            maxTargets = kMaxTargets;
        for (int targ = 0; targ < maxTargets; targ++) {
            for (int lun = 0; lun < kMaxLuns; lun++) {
                Inquiry inq;
                unsigned char deviceType;
                char addrString[48];
                bool deviceReady;

                dierr = GetDeviceType(ha, targ, lun, &deviceType);
                if (dierr != kDIErrNone)
                    continue;

                sprintf(addrString, "%d:%d:%d", ha, targ, lun);

                dierr = DeviceInquiry(ha, targ, lun, &inq);
                if (dierr != kDIErrNone) {
                    LOGI(" ASPI DeviceInquiry for '%s' (type=%d) failed",
                        addrString, deviceType);
                    continue;
                }

                LOGI("  Device %s is %s '%s' '%s'",
                    addrString, DeviceTypeToString(deviceType),
                    inq.vendorID, inq.productID);

                if ((deviceMask & kDevMaskCDROM) != 0 &&
                    deviceType == kScsiDevTypeCDROM)
                {
                    /* found CD-ROM */
                } else if ((deviceMask & kDevMaskHardDrive) != 0 &&
                    deviceType == kScsiDevTypeDASD)
                {
                    /* found hard drive */
                } else
                    continue;

                if (idx >= kMaxAccessibleDrives) {
                    LOGI("GLITCH: ran out of places to stuff CD-ROM drives");
                    assert(false);
                    goto done;
                }

                dierr = TestUnitReady(ha, targ, lun, &deviceReady);
                if (dierr != kDIErrNone) {
                    LOGI(" ASPI TestUnitReady for '%s' failed", addrString);
                    continue;
                }

                deviceArray[idx].Init(ha, targ, lun, inq.vendorID,
                    inq.productID, deviceType, deviceReady);
                idx++;
            }
        }
    }

done:
    *ppDeviceArray = deviceArray;
    *pNumDevices = idx;
    return kDIErrNone;
}

#endif /*_WIN32*/
