/*
 * Copyright (c) 1993 NeXT Computer, Inc.
 *
 * AHAThread.m - I/O thread methods for Adaptec 1542 driver. 
 *
 * HISTORY
 *
 * 13 Apr 1993	Doug Mitchell at NeXT
 *	Split off from AHAController.m.
 */

#import "AHAThread.h"
#import "AHATypes.h"
#import "AHAInline.h"
#import "AHAControllerPrivate.h"
#import "scsivar.h"
#import <driverkit/generalFuncs.h>
#import <driverkit/kernelDriver.h>
#import <kernserv/prototypes.h>
#import <sys/param.h>

static void ahaTimeout(void *arg);

#define AUTO_SENSE_ENABLE	1

/*
 * Template for timeout message.
 */
static msg_header_t timeoutMsgTemplate = {
	0,					// msg_unused 
	1,					// msg_simple 
	sizeof(msg_header_t),			// msg_size 
	MSG_TYPE_NORMAL,			// msg_type 
	PORT_NULL,				// msg_local_port 
	PORT_NULL,				// msg_remote_port - TO
						// BE FILLED IN 
	IO_TIMEOUT_MSG				// msg_id 
};

@implementation AHAController(IOThread)

/*
 * I/O thread version of -executeRequest:buffer:client.
 * The approximate logic is:
 *	Build up an internal ccb describing this request
 *	Put it on the queue of pending commands
 *	Run as many pending commands as possible
 *
 * Returns non-zero if no ccb was available for the command. This case 
 * must be handled gracefully by the caller by enqueueing the request on
 * commandQ.
 */
- (int)threadExecuteRequest	: (AHACommandBuf *)cmdBuf
{
	struct ccb	*ccb;
	IOSCSIRequest	*scsiReq = cmdBuf->scsiReq;
	
	ddm_thr("threadExecuteRequest cmdBuf 0x%x\n", cmdBuf, 2,3,4,5);
	
	ccb = [self allocCcb:(scsiReq->maxTransfer ? YES : NO)];
	if(ccb == NULL) {
		return 1;
	}
	if([self ccbFromCmd:cmdBuf ccb:ccb]) {
		/*
		 * Command reject. Error status is in 
		 * cmdBuf->scsiReq->driverStatus. 
		 * Notify caller and clean up.
		 */
		[self freeCcb:ccb];
		[cmdBuf->cmdLock lock];
		[cmdBuf->cmdLock unlockWith:CMD_COMPLETE];
		return 0;
	}

	/*
	 *  Make sure we'll be able to time this command out.  This should be
	 *  rare, so we don't particularly care about how efficient it is.
	 */
	ccb->timeoutPort = interruptPortKern;
	IOScheduleFunc(ahaTimeout, ccb, scsiReq->timeoutLength);

	/*
	 * Stick this command on the list of pending ones, and run them.
	 */
	queue_enter(&pendingQ, ccb, struct ccb *, ccbQ);
	[self runPendingCommands];
	return 0;

}

/*
 * I/O thread version of -resetSCSIBus.
 * We also interpret this to mean we should reset the board.
 * cmdBuf == NULL indicates a call from within the I/O thread for
 * a reason other than -resetSCSIBus (e.g., timeout recovery). 
 */
- (void)threadResetBus : (AHACommandBuf *)cmdBuf
{

	aha_ctrl_reg_t	ctrl = { 0 };
    	struct ccb *ccb;
	queue_head_t *q;
	
	ddm_thr("threadResetBus\n", 1,2,3,4,5);
	
	/*
	 * Abort all outstanding and pending commands. 
	 */
	for(q=&outstandingQ; q!=&pendingQ; q=&pendingQ)	{
		while(!queue_empty(q)) {
			ccb = (struct ccb *)queue_first(q);
			queue_remove(q, ccb, struct ccb *, ccbQ);
			if(q == &outstandingQ) {
				ASSERT(outstandingCount != 0);
				outstandingCount--;
			}
			[self commandCompleted:ccb reason:CS_Reset];
		}
	}
	
	/*
	 * Now reset the hardware.
	 */
	aha_reset_board(ioBase, ahaBoardId);
	aha_setup_mb_area(ioBase, ahaMbArea, ahaCcb);

	ctrl.scsi_rst = 1;
	aha_put_ctrl(ioBase, ctrl);

	IOLog("Resetting SCSI Bus...\n");
	IOSleep(10000);
	
	/*
	 * Notify caller of completion if appropriate.
	 */
	if(cmdBuf) {
		ddm_thr("threadResetBus: I/O complete on cmdBuf 0x%x\n",
			cmdBuf, 2,3,4,5);
		cmdBuf->result = SR_IOST_GOOD;
		[cmdBuf->cmdLock lock];
		[cmdBuf->cmdLock unlockWith:CMD_COMPLETE];
	}
}

/*
 * Build a ccb from the specified AHACommandBuf. Returns non-zero on error
 * (i.e., on command reject from this method). In that case, error status 
 * is in cmdBuf->scsiReq->driverStatus.
 */
- (int) ccbFromCmd:(AHACommandBuf *)cmdBuf ccb:(struct ccb *)ccb
{
	IOSCSIRequest		*scsiReq = cmdBuf->scsiReq;
	union cdb		*cdbp = &scsiReq->cdb;
	int			cdb_ctrl;
	vm_offset_t		addr, phys;
	vm_size_t		len;
	unsigned int		pages;
	unsigned int		cmdlen;
	
	/*
	 * Figure out what kind of cdb we've been given
	 * and snag the ctrl byte
	 */
	switch (SCSI_OPGROUP(cdbp->cdb_opcode)) {

	    case OPGROUP_0:
		cmdlen = sizeof (struct cdb_6);
		cdb_ctrl = cdbp->cdb_c6.c6_ctrl;
		break;
	
	    case OPGROUP_1:
	    case OPGROUP_2:
		cmdlen = sizeof (struct cdb_10);
		cdb_ctrl = cdbp->cdb_c10.c10_ctrl;
		break;
	
	    case OPGROUP_5:
		cmdlen = sizeof (struct cdb_12);
		cdb_ctrl = cdbp->cdb_c12.c12_ctrl;
		break;
    
    	    /*
	     * Group 6 and 7 commands allow a user-specified CDB length.
	     */
	    case OPGROUP_6:
		if(scsiReq->cdbLength)
		 	cmdlen = scsiReq->cdbLength;
		else
			cmdlen = sizeof (struct cdb_6);
		cdb_ctrl = 0;
		break;
    
	    case OPGROUP_7:
		if(scsiReq->cdbLength)
		 	cmdlen = scsiReq->cdbLength;
		else
			cmdlen = sizeof (struct cdb_10);
		cdb_ctrl = 0;
		break;
    
	    default:
		scsiReq->driverStatus = SR_IOST_CMDREJ;
		return 1;
	}

	/*
	 * Make sure nothing unreasonable has been asked of us
	 */
	if ((cdb_ctrl & CTRL_LINKFLAG) != CTRL_NOLINK) {
		scsiReq->driverStatus = SR_IOST_CMDREJ;
		return 1;
	}

	addr = (vm_offset_t)cmdBuf->buffer;
	len = scsiReq->maxTransfer;

	if (len > 0) 
		pages = (round_page(addr+len) - trunc_page(addr)) / PAGE_SIZE;
	else
		pages = 0;
	    
	ccb->cdb		= *cdbp;
	ccb->cdb_len		= cmdlen;
    
	ccb->data_in		= scsiReq->read;
	ccb->data_out		= !scsiReq->read;
	ccb->target		= scsiReq->target;
	ccb->lun		= scsiReq->lun;
	#if	AUTO_SENSE_ENABLE
	ccb->reqsense_len      = sizeof(esense_reply_t);
	#else	AUTO_SENSE_ENABLE
	ccb->reqsense_len	= 1;	/* no auto reqsense */
	#endif	AUTO_SENSE_ENABLE

	/* 
	 * Note Adaptec does not support command queueing. Synchronous
	 * negotiation can only be disabled by jumper. Disconnects can
	 * not be disabled.
	 */

	ccb->cmdBuf = cmdBuf;
	ccb->total_xfer_len = 0;
	IOGetTimestamp(&ccb->startTime);

	/*
	 *  Set up the DMA address and length.  If we have more than one page,
	 *  then chances are that we'll have to use scatter/gather to collect
	 *  all the physical pages into a single transfer.
	 */
	if (pages == 0) {
		aha_put_24(0, ccb->data_addr);
		aha_put_24(0, ccb->data_len);
		ccb->oper = AHA_CCB_INITIATOR_RESID;
	}
	else if (pages == 1) {		
	
		if(IOPhysicalFromVirtual(cmdBuf->client, addr, &phys)) {
			IOLog("%s: Can\'t get physical address\n",
				[self name]);
			scsiReq->driverStatus = SR_IOST_INT;
			return 1;
		}

		ccb->dmaList[0] = [self createDMABufferFor:&phys
				length:len read:scsiReq->read
				needsLowMemory:YES limitSize:NO];

		if (ccb->dmaList[0] == NULL) {
			[self abortDMA:ccb->dmaList length:len];
			scsiReq->driverStatus = SR_IOST_INT;
			return 1;
		}

		aha_put_24(phys, ccb->data_addr);
		aha_put_24(len, ccb->data_len);
	
		ccb->oper = AHA_CCB_INITIATOR_RESID;
		ccb->total_xfer_len = len;
	}
	else {
		vm_offset_t	lastPhys = 0;
		unsigned int	sgEntry = 0;
		unsigned int	maxEntries = MIN(pages, AHA_SG_COUNT);
		IOEISADMABuffer	*dmaBuf = ccb->dmaList;

		for (sgEntry=0;  sgEntry < maxEntries;  sgEntry++) {
			struct aha_sg	*sg = &ccb->sg_list[sgEntry];
			unsigned int	thisLength;
			
			thisLength = MIN(len, round_page(addr+1) - addr);
			
	   		if(IOPhysicalFromVirtual(cmdBuf->client, 
					addr, &phys)) {
				IOLog("%s: Can\'t get physical address\n",
					[self name]);
				[self abortDMA:ccb->dmaList
					length:ccb->total_xfer_len];
				scsiReq->driverStatus = SR_IOST_INT;
				return 1;
			}
			*dmaBuf = [self createDMABufferFor:&phys
					length:thisLength
					read:scsiReq->read
					needsLowMemory:YES limitSize:NO];

			if (*dmaBuf == NULL) {
				[self abortDMA:ccb->dmaList
					length:ccb->total_xfer_len];
				scsiReq->driverStatus = SR_IOST_INT;
				return 1;
			}

			aha_put_24(phys, sg->addr);
			aha_put_24(thisLength, sg->len);
    
			ccb->total_xfer_len += thisLength;
			
			addr += thisLength;
			len -= thisLength;
			lastPhys = phys;
			dmaBuf++;
		}
	
		if(IOPhysicalFromVirtual(IOVmTaskSelf(), 
				(unsigned)ccb->sg_list, 
				&phys)) {
			IOLog("%s: Can\'t get physical address of ccb\n", 
				[self name]);
			IOPanic("AHAController");
		}
		aha_put_24(phys, ccb->data_addr);
		aha_put_24(sgEntry * sizeof(struct aha_sg), ccb->data_len);

		ccb->oper = AHA_CCB_INITIATOR_RESID_SG;
	}

	return 0;
}

/*
 * If any commands pending, and the controller's queue is not full,
 * run the new commands. 
 */
- runPendingCommands
{
	unsigned int	cmdsToRun;
	struct ccb	*ccb;

	cmdsToRun = AHA_QUEUE_SIZE - outstandingCount;

	while (cmdsToRun > 0 && !queue_empty(&pendingQ)) {

		/*
		 *  Dequeue pending command and add to the outstanding queue.
		 */
		ccb = (struct ccb *) queue_first(&pendingQ);
		queue_remove(&pendingQ, ccb, struct ccb *, ccbQ);
		if (!ccb)
			break;

		queue_enter(&outstandingQ, ccb, struct ccb *, ccbQ);
		outstandingCount++;
		
		/*
		 *  Let 'er rip...
		 */
		ccb->mb_out->mb_stat = AHA_MB_OUT_START;
		aha_start_scsi(ioBase);
	
		/*
		 *  Accumulate some simple statistics: the max queue length
		 *  and enough info to compute a running average of the queue
		 *  length.
		 */
		maxQueueLen = MAX(maxQueueLen, outstandingCount);
		queueLenTotal += outstandingCount;
		totalCommands++;

		cmdsToRun--;
	}
	return self;
}

/*
 * A command is done.  Figure out what happened, and notify the
 * client appropriately. Called upon detection of I/O complete interrupt,
 * timeout detection, or when we reset the bus and blow off pending 
 * commands.
 */
- (void)commandCompleted : (struct ccb *) ccb 
	          reason : (completeStatus)reason
{
	ns_time_t		currentTime;
	IOSCSIRequest		*scsiReq;
	AHACommandBuf  		*cmdBuf = ccb->cmdBuf;
	
	ASSERT(cmdBuf != NULL);
	scsiReq = cmdBuf->scsiReq;
	ASSERT(scsiReq != NULL);

	ddm_thr("commandCompleted: ccb 0x%x cmdBuf 0x%x reason %d\n", 
		ccb, cmdBuf, reason, 4,5);
		
	scsiReq->scsiStatus = ccb->target_status;

	switch(reason) {
	    case CS_Timeout:
	   	scsiReq->driverStatus = SR_IOST_IOTO;
		break;
	    case CS_Reset:
	    	scsiReq->driverStatus = SR_IOST_RESET;
		break;
	    case CS_Complete:
		switch (ccb->host_status) {
    
		/*
		 * Handle success and data overrun/underrun.  We can handle
		 * overrun/underrun as a normal case because the controller 
		 * sets the data_len field to be the actual number of bytes 
		 * transferred regardless of overrun.
	         */
		case AHA_HOST_SUCCESS:
		case AHA_HOST_DATA_OVRUN:
		    [self completeDMA:ccb->dmaList 
		    	length:scsiReq->maxTransfer];
		    scsiReq->bytesTransferred = ccb->total_xfer_len -  
					aha_get_24(ccb->data_len);
    
		    /*
		     *  Everything looks good.  Make sure the SCSI status byte
		     *  is cool before we really say everything is hunky-dory.
		     */
		    if (scsiReq->scsiStatus == STAT_GOOD)
			    scsiReq->driverStatus = SR_IOST_GOOD;
		    else if (scsiReq->scsiStatus == STAT_CHECK) {
		        if(AUTO_SENSE_ENABLE) {
			
			    esense_reply_t *sensePtr;
			    
			    scsiReq->driverStatus = SR_IOST_CHKSV;
			    
			    /*
			     * Sense data starts immediately after the actual
			     * cdb area we use, not an entire union cdb.
			     */
			    sensePtr = (esense_reply_t *)
			    	(((char *)&ccb->cdb) + ccb->cdb_len);
			    scsiReq->senseData = *sensePtr;
			}
			else {
			    scsiReq->driverStatus = SR_IOST_CHKSNV;
			}
		    }
		    else
			    scsiReq->driverStatus = ST_IOST_BADST;
		    break;
    
		case AHA_HOST_SEL_TIMEOUT:
		    [self abortDMA:ccb->dmaList length:scsiReq->maxTransfer];
		    scsiReq->driverStatus = SR_IOST_SELTO;
		    break;
		
		default:
		    IOLog("AHA interrupt: bad status %x\n", ccb->host_status);
		    [self abortDMA:ccb->dmaList length:scsiReq->maxTransfer];
		    scsiReq->driverStatus = SR_IOST_INVALID;
		    break;
	    }   /*  switch host_status */
	}   	/*  switch status */

	IOGetTimestamp(&currentTime);
	scsiReq->totalTime = currentTime - ccb->startTime;
	cmdBuf->result = scsiReq->driverStatus;
	
	/*
	 * Wake up client.
	 */
	ddm_thr("commandCompleted: I/O complete on cmdBuf 0x%x\n",
			cmdBuf, 2,3,4,5);
	[cmdBuf->cmdLock lock];
	[cmdBuf->cmdLock unlockWith:CMD_COMPLETE];

	/*
	 * Free the CCB and clean up possible pending timeout.
	 */	
	(void) IOUnscheduleFunc(ahaTimeout, ccb);
	[self freeCcb:ccb];
}

/*
 * Alloc/free ccb's. These only come from the array ahaCcb[]. 
 * If we can't find one, return NULL - caller will have to try 
 * again later.
 */
- (struct ccb *)allocCcb : (BOOL)doDMA
{
	struct ccb *ccb;
	int i;
	
	if(numFreeCcbs == 0) {
		ddm_thr("allocCcb: numFreeCcbs = 0\n", 1,2,3,4,5);
		return NULL;
	}

	/*
	 * Since numFreeCcbs is non-zero, there has to be one available 
	 * in ahaCcb[].
	 */
	ccb = ahaCcb;
	while (ccb <= &ahaCcb[AHA_QUEUE_SIZE - 1] && ccb->in_use) {
		ccb++;
	}
	if (ccb > &ahaCcb[AHA_QUEUE_SIZE - 1]) {
		IOPanic("AHAController: out of ccbs");
	}
	numFreeCcbs--;
	ccb->in_use = TRUE;
	
	/*
	 * Null out dmaList.
	 */
	for(i=0; i<AHA_SG_COUNT; i++) {
		ccb->dmaList[i] = NULL;
	}
	
	/*
	 * Acquire the reentrant DMA lock. This is a nop on EISA machines.
	 * 
	 * Although -reserveDMALock is reentrant for multiple threads on
	 * one device, it is *not* reentrant for one thread. Thus we should 
	 * only call it if we don't already hold the lock.
	 * Also, avoid this if we're not going to do any DMA. 
	 */
	if(doDMA && (++dmaLockCount == 1)) {
		ddm_thr("allocCcb: calling reserveDMALock\n", 1,2,3,4,5);
		[super reserveDMALock];
	}
	ddm_thr("allocCcb: returning 0x%x\n", ccb, 2,3,4,5);
	
	return ccb;	
}

- (void)freeCcb : (struct ccb *)ccb
{
	BOOL	didDMA = (ccb->total_xfer_len ? YES : NO);

	ddm_thr("freeCcb: ccb 0x%x\n", ccb, 2,3,4,5);
	ccb->in_use = FALSE;
	numFreeCcbs++;
	if(didDMA && (--dmaLockCount == 0)) {
		ddm_thr("freeCcb: calling releaseDMALock\n", 
			1,2,3,4,5);
		[super releaseDMALock];
	}
}

- (void) completeDMA:(IOEISADMABuffer *) dmaList length:(unsigned int) xferLen
{
	IOEISADMABuffer	*buf = &dmaList[0];
	int		i;
	
	for (i = 0; i < AHA_SG_COUNT; i++, buf++) {
		if(*buf) {
			[self freeDMABuffer:*buf];
		}
		else {
			return;
		}
	}
}


- (void) abortDMA:(IOEISADMABuffer *) dmaList length:(unsigned int) xferLen
{
	IOEISADMABuffer	*buf = &dmaList[0];
	int		i;
	
	for (i = 0; i < AHA_SG_COUNT; i++, buf++) {
		if(*buf) {
			[self abortDMABuffer:*buf];
		}
		else {
			return;
		}
	}
}

@end


/*
 *  Handle timeouts.  We just send a timeout message to the I/O thread
 *  so it wakes up.
 */
static void
ahaTimeout(void *arg)
{
	
	struct ccb	*ccb = arg;
	msg_header_t	msg = timeoutMsgTemplate;
	msg_return_t	mrtn;
	
	if(!ccb->in_use) {
		/* 
		 * Race condition - this CCB got completed another way. 
		 * No problem.
		 */
		return;
	}	
	msg.msg_remote_port = ccb->timeoutPort;
	IOLog("AHA timeout\n");
	if(mrtn = msg_send_from_kernel(&msg, MSG_OPTION_NONE, 0)) {
		IOLog("ahaTimeout: msg_send_from_kernel() returned %d\n",
			mrtn);
	}
}

