/* Copyright (c) 1993 NeXT Computer, Inc.  All rights reserved.
 *
 * SCSITapeKern.m -- implementation of scsi tape driver entry point routines
 *
 * HISTORY
 * 31-Mar-93	Phillip Dibner at NeXT
 *	Created.   Adapted from st.c, created by Doug Mitchell at NeXT.
 *
 */ 
 
/*
 * Four different devices are implemented here:
 *
 *	rst - generic SCSI tape, rewind on close
 *	nrst - generic SCSI tape, no rewind on close 
 *	rxt - Exabyte SCSI tape, rewind on close
 *	nrxt - Exabyte SCSI tape, no rewind on close
 *
 *	All 4 devices have the same major number. Bit 0 of the minor number 
 *	selects "rewind on close" (0) or "no rewind" (1). Bit 1 of the 
 *	minor number select generic (0) or Exabyte (1).
 *
 *	The Exabyte drive currently requires these actions on open:
 *
 *		-- enable Buffered Write mode
 *		-- Inhibit Illegal Length errors
 *		-- Disable Disconnect During Data Transfer
 */	

#import <sys/errno.h>
#import <sys/types.h>
#import <sys/buf.h>
#import <sys/conf.h>
#import <sys/uio.h>
#import <sys/mtio.h>
#import <bsd/dev/scsireg.h>

#import <driverkit/scsiTypes.h>
#import <driverkit/align.h>
#import <driverkit/kernelDriver.h>
#import <driverkit/scsiTypes.h>
#import <driverkit/return.h>
#import <driverkit/devsw.h> 
#import <kernserv/prototypes.h>
#import "SCSITape.h"


#define USE_EBD	1		/* use "even byte diconnect" rather than 
				 * "no disconnect during data xfer" for exabyte
				 */

/*
 * Unix-style entry points
 */
int stopen (dev_t dev);
int stclose (dev_t dev);
int stread (dev_t dev, struct uio *uiop);
int stwrite (dev_t dev, struct uio *uiop);
int stioctl (dev_t dev, int cmd, caddr_t data, int flag);

/*
 * Subsidiary functions used by the kernel "glue" layer
 */
static int st_rw (dev_t dev, struct uio *uiop, int rw_flag);
static int st_doiocsrq (id scsiTape, scsi_req_t *srp);

/*
 * Functions to take care of byte-ordering issues
 */				 
extern void assign_cdb_c6s_len();
extern void assign_msbd_numblocks();
extern void assign_msbd_blocklength();
unsigned int read_er_info_low_24();

extern id		stIdMap[];


/*
 * Add ourself to cdevsw. Called from SCSIGeneric layer at probe time. 
 */
extern int		nulldev();
extern int		nodev();

static int stMajor = -1;

int 
st_devsw_init()
{
    int		rtn;
    
    /*
     * We get called once for each IOSCSIController in the system; we
     * only have to call IOAddToCdevsw() once.
     */
    if(stMajor >= 0) {
    	return stMajor;
    }
    rtn = IOAddToCdevsw ((IOSwitchFunc) stopen,
	(IOSwitchFunc) stclose, 
	(IOSwitchFunc) stread,
	(IOSwitchFunc) stwrite,
	(IOSwitchFunc) stioctl,
	(IOSwitchFunc) nodev,
	(IOSwitchFunc) nulldev,		// reset
	(IOSwitchFunc) nulldev,
	(IOSwitchFunc) nodev,		// mmap
	(IOSwitchFunc) nodev,		// getc
	(IOSwitchFunc) nodev);		// putc
    if(rtn < 0) {
	IOLog("st: Can't find space in devsw\n");
    }
    else {
	IOLog("st: major number %d\n", rtn);
	stMajor = rtn;
    }
    return rtn;
}


int
stopen(dev_t dev)
{
    int			unit = ST_UNIT(dev); 
    id			scsiTape = stIdMap[unit];

    if([scsiTape acquireDevice] == IO_R_BUSY)
	return(EBUSY);			/* already open */
    if ((unit >= NST) || 			/* illegal device */
	([scsiTape isInitialized] == NO)) {	/* hasn't been init'd */
	    [scsiTape releaseDevice];
	    return(ENXIO);			/* FIXME - try to init here */
    }

    /*
     * We send this once, and ignore result, to clear check condition
     * due to media change, etc.
     */
    [scsiTape setIgnoreCheckCondition: YES];
    [scsiTape stTestReady];
    [scsiTape setIgnoreCheckCondition: NO];
	
    if(ST_EXABYTE(dev)) {
	struct modesel_parms		*mspp;
	struct exabyte_vudata		*evudp;
	struct mode_sel_hdr		*mshp;

	mspp = IOMalloc (sizeof (struct modesel_parms));
	evudp = (struct exabyte_vudata *) &mspp->msp_data.msd_vudata;
		
	/* 
	 * Exabyte "custom" setup
	 */

	/* Set variable block size */
	if([scsiTape setBlockSize: 0] != IO_R_SUCCESS) {
	    IOFree (mspp, sizeof (struct modesel_parms));
	    [scsiTape releaseDevice];

#ifdef DEBUG
IOLog ("stopen: cannot set block size variable\n");
#endif DEBUG

	    return(EIO);
	}

	/* Suppress illegal length errors */
	[scsiTape setSuppressIllegalLength: YES];
		
	/* Do a mode sense */
	mspp->msp_bcount = sizeof(struct mode_sel_hdr) + 
	    sizeof(struct mode_sel_bd) + MSP_VU_EXABYTE;

	if([scsiTape stModeSense: mspp] != SR_IOST_GOOD) {
	    IOFree (mspp, sizeof (struct modesel_parms));
	    [scsiTape releaseDevice];

#ifdef DEBUG
IOLog ("stopen: Mode Sense failed\n");
#endif DEBUG

	    return(EIO);
	}
			
	/* some fields we have to zero as a matter of course */	
	mshp = &mspp->msp_data.msd_header;	
	mshp->msh_sd_length_0 = 0;
	mshp->msh_med_type = 0;
	mshp->msh_wp = 0;
	mshp->msh_bd_length = sizeof(struct mode_sel_bd);
	assign_msbd_blocklength (&mspp->msp_data.msd_blockdescript, 0);
	assign_msbd_numblocks (&mspp->msp_data.msd_blockdescript, 0);
		
	/*
	 * set up buffered mode, #blocks = 0, even byte disconnect,
	 * enable parity; do mode selsect
	 */
	mspp->msp_data.msd_header.msh_bufmode = 1;

#ifdef	USE_EBD
	/* clear NDD and set EBD; enable parity  */
	evudp->nd = 0;		/* disconnects OK */
	evudp->ebd = 1;		/* but only on word boundaries */
	evudp->pe = 1;		/* parity enabled */
	evudp->nbe = 1;		/* Busy status disabled */
#else	USE_EBD
	evudp->nd = 1;
#endif	USE_EBD
	if([scsiTape stModeSelect: mspp] != SR_IOST_GOOD) {
	    IOFree (mspp, sizeof (struct modesel_parms));
	    [scsiTape releaseDevice];

#ifdef DEBUG
IOLog ("stopen: Mode Select failed\n");
#endif DEBUG

	    return(EIO);
	}
	IOFree (mspp, sizeof (struct modesel_parms));
    }
    return(0);
}




int
stclose(dev_t dev)
{
    int			unit = ST_UNIT(dev); 
    id			scsiTape = stIdMap[unit];
    int			rtn = 0;
	
    if ([scsiTape didWrite] == YES) {
	/* we must write a file mark to close the file */
	if ([scsiTape stCloseFile] != SR_IOST_GOOD) {
	    rtn = EIO;
	}
    }

    if(ST_RETURN(dev) == 0) {		/* returning device? */
	if ([scsiTape stRewind] != SR_IOST_GOOD) {
	    rtn = EIO;
	}
    }

    [scsiTape releaseDevice];
    return(rtn);
}


int
stread(dev_t dev, struct uio *uiop)
{
    return(st_rw(dev,uiop,SR_DMA_RD));
}

int
stwrite(dev_t dev, struct uio *uiop)
{
    return(st_rw(dev,uiop,SR_DMA_WR));
}


static int
st_rw(dev_t dev, struct uio *uiop, int rw_flag) {

    int 			unit = ST_UNIT(dev); 
    id				scsiTape = stIdMap[unit];
    IOSCSIRequest	 	scsiReq;
    struct cdb_6s		*cdbp = &scsiReq.cdb.cdb_c6s;
    void 			*freePtr;
    int 			freeCnt;
    unsigned char		*alignedBuf;
    IODMAAlignment		dmaAlign;
    int				length;
    int				rtn = 0;

sc_status_t scRet = -1;
	
    if (unit >= NST) 
	return(ENXIO);
    if(uiop->uio_iovcnt != 1)		/* single requests only */ 
	return(EINVAL);
    if(uiop->uio_iov->iov_len == 0) 
	return(0);			/* nothing to do */

#ifdef	DEBUG
//	if(rw_flag == SR_DMA_RD) {
//		XCDBG(("st: READ; count = %xH\n", uiop->uio_iov->iov_len));
//	}
//	else {
//		XCDBG(("st: WRITE; count = %xH\n", uiop->uio_iov->iov_len));
//	}
#endif	DEBUG

    /*
     * FIXME: should wire user's memory and DMA from there, avoiding
     * a copyin() or copyout().
     */

    alignedBuf = [[scsiTape controller]
	allocateBufferOfLength: uiop->uio_iov->iov_len
	actualStart: &freePtr
	actualLength: &freeCnt];


    bzero(&scsiReq, sizeof(IOSCSIRequest));

    scsiReq.target 		= [scsiTape target];
    scsiReq.lun 		= [scsiTape lun];

    [[scsiTape controller] getDMAAlignment:&dmaAlign];
    if(dmaAlign.readLength > 1) {
	scsiReq.maxTransfer = IOAlign(int, uiop->uio_iov->iov_len, 
	    dmaAlign.readLength);

    } else {
	scsiReq.maxTransfer = uiop->uio_iov->iov_len;
    }

    scsiReq.timeoutLength = ST_IOTO_NORM;
    scsiReq.disconnect = 1;
    cdbp->c6s_lun = [scsiTape lun];

    if ([scsiTape isFixedBlock]) {
	/* c6s_len is BLOCK COUNT */
	length = howmany(uiop->uio_iov->iov_len, [scsiTape blockSize]);
	cdbp->c6s_opt = C6OPT_FIXED;

#ifdef	DEBUG
IOLog ("SCSI Tape read/write: set up for fixed block transfer\n");
#endif	DEBUG


    } else {
	length = uiop->uio_iov->iov_len;
	if(rw_flag == SR_DMA_RD)
	    if ([scsiTape suppressIllegalLength]) {
		cdbp->c6s_opt |= C6OPT_SIL;

#ifdef	DEBUG
IOLog ("SCSI Tape read: variable block read, suppress illegal len errs\n");
#endif	DEBUG

	    }
	    else {

#ifdef	DEBUG
IOLog ("SCSI Tape read: variable block read, allow illegal len errs\n");
#endif	DEBUG

	    }
    }
    assign_cdb_c6s_len (cdbp, length);

#ifdef DEBUG
IOLog ("Transfer Length is %d\n", length);
#endif DEBUG

    if(length > C6S_MAXLEN) {
	rtn = EINVAL;
	goto out;
    }

    if(rw_flag == SR_DMA_RD) {
	cdbp->c6s_opcode  = C6OP_READ;
	scsiReq.read = YES;
    }
    else {
	cdbp->c6s_opcode  = C6OP_WRITE;
	scsiReq.read = NO;
	
    }

    scsiReq.bytesTransferred = 0;

    /* Copy user data to kernel space if write. */
    if(rw_flag == SR_DMA_WR)
	if((rtn = copyin(uiop->uio_iov->iov_base, alignedBuf,
	    uiop->uio_iov->iov_len)))
		goto out;

    if ((scRet = [scsiTape executeRequest: &scsiReq
	buffer: alignedBuf
	client: IOVmTaskSelf()
	senseBuf: [scsiTape senseDataPtr]]) != SR_IOST_GOOD) {

	rtn = EIO;

#ifdef	DEBUG
IOLog ("st_rw: returned on failure from executeRequest\n");
IOLog ("st_rw:  ---- returned %d\n", scRet);
#endif	DEBUG


	goto out;
    }

    /* It worked. Copy data to user space if read. */
    if(scsiReq.bytesTransferred && (rw_flag == SR_DMA_RD)) {
	rtn = copyout(alignedBuf, uiop->uio_iov->iov_base,
	    scsiReq.bytesTransferred);

#ifdef DEBUG
IOLog ("return value from copyout is %d\n", rtn);
#endif DEBUG

    }

    if(scsiReq.driverStatus != SR_IOST_GOOD) {	// XXX Can this happen?
	rtn = EIO;
    }

out:

#ifdef DEBUG
IOLog ("SCSI st_rw transferred %d bytes out of %d\n",
    scsiReq.bytesTransferred, uiop->uio_iov->iov_len);
#endif DEBUG

    uiop->uio_resid = uiop->uio_iov->iov_len - scsiReq.bytesTransferred;
    IOFree (freePtr, freeCnt);
    IOSetUNIXError (rtn);
    return rtn;

} /* st_rw() */


/*
 * ioctl for SCSI Tape.    
 * XXX sc_return_t to errno conversions could use more review.
 */
int
stioctl(dev_t dev, 
    int cmd, 			/* MTIOCTOP, etc */
    caddr_t data, 		/* actually a ptr to mt_op or mtget, if used */
    int flag)			/* for historical reasons. Not used. */
{
    int				error = 0;
    int				unit = ST_UNIT(dev); 
    id				scsiTape = stIdMap[unit];
    struct mtget		*mgp = (struct mtget *)data;
    struct esense_reply		*erp;
    sc_status_t			scsi_err;
	

    if (unit >= NST) 
	return(ENXIO);
    switch (cmd) {
	case MTIOCTOP:			/* do tape op */

	    if ((scsi_err =
		[scsiTape executeMTOperation: (struct mtop *) data]) != 
		SR_IOST_GOOD) {

		if (scsi_err == SR_IOST_CMDREJ) {
		    error = EINVAL;
		} else {
		    error = EIO;
		}
	    }
	    break;
		
	case MTIOCGET:			/* get status */

	    erp = [scsiTape senseDataPtr];

	    /* 
	     * If we just did a request sense command as part of 
	     * error recovery, avoid doing another one and
	     * thus blowing away possible volatile status info.
	     */
	    if([scsiTape senseDataValid] == NO) {
		if((scsi_err = [scsiTape requestSense: erp]) != SR_IOST_GOOD) {
		    error = EIO;
		    break;
		}
	    }
				
	    /*
	     * [scsiTape senseDataPtr] now definitely contains valid
	     * sense data.
	     */
	    if(ST_EXABYTE(dev)) 
		mgp->mt_type = MT_ISEXB;
	    else
		mgp->mt_type = MT_ISGS;
	    mgp->mt_dsreg = ((u_char *)erp)[2];
	    mgp->mt_erreg = erp->er_addsensecode;
	    mgp->mt_ext_err0 = (((u_short)erp->er_stat_13) << 8) |
		((u_short)erp->er_stat_14);
	    mgp->mt_ext_err1 = (((u_short)erp->er_stat_15) << 8) |
		((u_short)erp->er_rsvd_16);

#if	__BIG_ENDIAN__
	    mgp->mt_resid = (u_int) erp->er_info;
#elif	__LITTLE_ENDIAN__
	    mgp->mt_resid = read_er_info_low_24();
	    mgp->mt_resid |= (u_int) erp->er_info3;
#endif
				    
	    /* force actual request sense next time */
	    [scsiTape forceSenseDataInvalid];
	    break;
		
	case MTIOCFIXBLK:			/* set fixed block mode */
	    error = [scsiTape 
		errnoFromReturn: [scsiTape setBlockSize: *(int *)data]];
	    break;

	case MTIOCVARBLK:			/* set variable block mode */
	    error = [scsiTape 
		errnoFromReturn: [scsiTape setBlockSize: 0]];
	    break;

	case MTIOCINILL:			/* inhibit illegal length
	    					 *    errors on Read */
	    [scsiTape setSuppressIllegalLength: YES];
	    break;
		
	case MTIOCALILL:			/* allow illegal length
	    					 *    errors on Read */
	    [scsiTape setSuppressIllegalLength: NO];
	    break;

	case MTIOCMODSEL:			/* mode select */
	    error = 0;
	    if ([scsiTape stModeSelect: (struct modesel_parms *)data] !=
		SR_IOST_GOOD) {

		error = EIO; 
		break;  
	    }	
	    	
	case MTIOCMODSEN:			/* mode sense */
	    error = 0;
	    if ([scsiTape stModeSense: (struct modesel_parms *)data] !=
		SR_IOST_GOOD) {

		error = EIO; 
		break;  
	    }	
	    	
	case MTIOCSRQ:				/* I/O via scsi_req */
	    error = st_doiocsrq(scsiTape, (struct scsi_req *) data);
	    break;

	default:
	    error = EINVAL;			/* invalid argument */
	    break;
    }
    IOSetUNIXError (error);	/* XXX Probably not necessary */
    return error;
} /* stioctl() */



/* 
 * Lifted directly from sg driver.
 *
 * Execute one scsi_req. Called from client's task context. Returns an errno.
 */

/*
 * FIXME - DMA to non-page-aligned user memory doesn't work. There
 * is data corruption on read operations; the corruption occurs on page
 * boundaries. 
 */
#define FORCE_PAGE_ALIGN	1
#if	FORCE_PAGE_ALIGN
int stForcePageAlign = 1;
#endif	FORCE_PAGE_ALIGN

static int st_doiocsrq(id scsiTape, scsi_req_t *srp)
{
    void 		*alignedPtr = NULL;
    unsigned 		alignedLen = 0;
    void 		*freePtr;
    unsigned 		freeLen;
    BOOL 		didAlign = NO;
    vm_task_t		client = NULL;
    int			rtn = 0;
    IOSCSIRequest	scsiReq;
    sc_status_t		srtn;
	
    if(srp->sr_dma_max > [[scsiTape controller] maxTransfer]) {
	return EINVAL;
    }
	
    /* Get some well-aligned memory if necessary. By using 
     * allocateBufferOfLength we guarantee that there is enough space 
     * in the buffer we pass to the controller to handle 
     * end-of-buffer alignment, although we won't copy more 
     * than sr_dma_max to or from the  caller.
     */
    if(srp->sr_dma_max != 0) {

	IODMAAlignment dmaAlign;
	id controller = [scsiTape controller];
	unsigned alignLength;
	unsigned alignStart;

	/*
	 * Get appropriate alignment from controller.
	 */
	[[scsiTape controller] getDMAAlignment:&dmaAlign];
	if(srp->sr_dma_dir == SR_DMA_WR) {
	    alignLength = dmaAlign.writeLength;
	    alignStart  = dmaAlign.writeStart;
	}
	else {
	    alignLength = dmaAlign.readLength;
	    alignStart  = dmaAlign.readStart;
	}
#if	FORCE_PAGE_ALIGN
	if(stForcePageAlign) {
	    alignStart = PAGE_SIZE;
	}
#endif	FORCE_PAGE_ALIGN
	if( ( (alignStart > 1) && 
		!IOIsAligned(srp->sr_addr, alignStart)
	    ) ||
	    ( (alignLength > 1) && 
		!IOIsAligned(srp->sr_dma_max, alignLength)
	    ) ||
		/*
`		 * XXX Prevent DMA from user space for now, even if the 
		 * buffer is well-aligned.  We need to wire down the user
		 * memory if we are going to DMA from it.
		 */
		YES
	    ) {

	    /* 
	     * DMA from kernel memory, we allocate and copy.
	     */
			
	    didAlign = YES;
	    client = IOVmTaskSelf();
			
	    if(alignLength > 1) {
		alignedLen = IOAlign(unsigned,
		    srp->sr_dma_max,
		    alignLength);
		}
		else {
		    alignedLen = srp->sr_dma_max;
		}	
		alignedPtr = [controller allocateBufferOfLength:
		    srp->sr_dma_max
		    actualStart:&freePtr
		    actualLength:&freeLen];
		if(srp->sr_dma_dir == SR_DMA_WR) {
		    rtn = copyin(srp->sr_addr, alignedPtr,
			srp->sr_dma_max);
		if(rtn) {
		    rtn = EFAULT;
		    goto err_exit;
		}
	    }
	}
	else {
	    /*
	     * Well-aligned buffer, DMA directly to/from user 
	     * space.
	     */
	    alignedLen = srp->sr_dma_max;
	    alignedPtr = srp->sr_addr;
	    client = IOVmTaskCurrent();
	    didAlign = NO;
	}
    } 

    /*
     * Generate a contemporary version of scsi_req.
     */
    bzero(&scsiReq, sizeof(scsiReq));
    scsiReq.target = [scsiTape target];
    scsiReq.lun    = [scsiTape lun];
	
    /*
     * Careful. this assumes that the old and new cdb structs are
     * equivalent...
     */
    scsiReq.cdb = srp->sr_cdb;
    scsiReq.read = (srp->sr_dma_dir == SR_DMA_RD) ? YES : NO;
    scsiReq.maxTransfer = alignedLen;
    scsiReq.timeoutLength = srp->sr_ioto;
    scsiReq.disconnect = 1;
	
    /*
     * Go for it.
     *
     * XXX Should use the SCSITape object's sense buffer, because
     * that's where MTIOCGET looks for valid sense data, and then
     * copy back the sense data to the old-style scsi_req's sense
     * buffer.
     */
    srtn = [scsiTape executeRequest:&scsiReq
	buffer : alignedPtr
	client : client
	senseBuf : &srp->sr_esense];
	
    /*
     * Copy status back to user. Note that if we got this far, we
     * return good status from the function; errors are in 
     * srp->sr_io_status.
     */
    srp->sr_io_status = srtn;
    srp->sr_scsi_status = scsiReq.scsiStatus;
    srp->sr_dma_xfr = scsiReq.bytesTransferred;
    if(srp->sr_dma_xfr > srp->sr_dma_max) {
	srp->sr_dma_xfr = srp->sr_dma_max;
    }
    ns_time_to_timeval(scsiReq.totalTime, &srp->sr_exec_time);

    /*
     * Copy read data back to user if appropriate.
     */
    if((srp->sr_dma_dir == SR_DMA_RD) && 
	(scsiReq.bytesTransferred != 0) && didAlign) {

	rtn = copyout(alignedPtr, 
	    srp->sr_addr, 
	    srp->sr_dma_xfr);
    }
err_exit:
    if(didAlign) {
	IOFree(freePtr, freeLen);
    }
    return rtn;
}


/*
 * Supporting function for managing byte-order swapping.
 */
unsigned int
read_er_info_low_24(struct esense_reply *erp)
{
#if	__BIG_ENDIAN__
    return ((unsigned int) erp->er_info);
#elif	__LITTLE_ENDIAN__
    return (unsigned int)
	(erp->er_info2 << 16) + (erp->er_info1 << 8) + erp->er_info0;

#endif

}
