/*
 * 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"
#ifdef _WIN32

#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 == nil) {
		DWORD lastErr = ::GetLastError();
		if (lastErr == ERROR_MOD_NOT_FOUND) {
			WMSG1("ASPI DLL '%s' not found\n", kASPIDllName);
		} else {
			WMSG2("ASPI LoadLibrary(%s) failed (err=%ld)\n",
				kASPIDllName, GetLastError());
		}
		return kDIErrGeneric;
	}

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

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

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

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

	return kDIErrNone;
}

/*
 * Destructor.  Unload the ASPI DLL.
 */
ASPI::~ASPI(void)
{
	if (fhASPI != nil) {
		WMSG0("Unloading ASPI DLL\n");
		::FreeLibrary(fhASPI);
		fhASPI = nil;
	}
}


/*
 * 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) {
		WMSG2("ASPI(SC_HA_INQUIRY on %d) failed with result=0x%lx\n",
			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 != nil);

	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 = nil;
	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.
			WMSG3(" ASPI TestUnitReady: drive %d:%d:%d is NOT ready\n",
				adapter, target, lun);
		} else {
			WMSG3(" ASPI TestUnitReady failed, status=0x%02x sense=0x%02x ASC=0x%02x\n",
				srb.SRB_TargStat, pSense->senseKey,
				pSense->additionalSenseCode);
		}
		*pReady = false;
	} else {
		const CDB_SenseData* pSense = (const CDB_SenseData*) srb.SenseArea;
		WMSG3(" ASPI TestUnitReady: drive %d:%d:%d is ready\n",
			adapter, target, lun);
		//WMSG3("   status=0x%02x sense=0x%02x ASC=0x%02x\n",
		//	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;

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

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

	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;

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

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

	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 = nil;
	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 == nil) {
		WMSG0("Failed creating a completion event?\n");
		return kDIErrGeneric;
	}

	pSRB->SRB_PostProc = completionEvent;

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

		::CloseHandle(completionEvent);

		if (eventStatus == WAIT_TIMEOUT) {
			WMSG0("  ASPI exec timed out!\n");
			return kDIErrSCSIFailure;
		} else if (eventStatus != WAIT_OBJECT_0) {
			WMSG1("  ASPI exec returned weird wait state %ld\n", 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;

		WMSG4("  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, ...
		WMSG3("  ASPI failed on command 0x%02x: aspiStatus=%d scsiStatus=%d\n",
			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 = nil;
	int idx = 0;

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

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

	WMSG1("ASPI scanning %d host adapters\n", fHostAdapterCount);

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

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

		WMSG2(" ASPI host adapter %d (SCSI ID=%d)\n", ha, adi.adapterScsiID);
		WMSG2("   identifier='%s' managerID='%s'\n",
			adi.identifier, adi.managerID);
		WMSG2("   maxTargets=%d bufferAlignment=%d\n",
			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) {
					WMSG2(" ASPI DeviceInquiry for '%s' (type=%d) failed\n",
						addrString, deviceType);
					continue;
				}

				WMSG4("  Device %s is %s '%s' '%s'\n",
					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) {
					WMSG0("GLITCH: ran out of places to stuff CD-ROM drives\n");
					assert(false);
					goto done;
				}

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

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

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

#endif /*_WIN32*/
