/* 
 *   Creation Date: <2003/07/09 23:40:42 samuel>
 *   Time-stamp: <2003/08/01 20:44:36 samuel>
 *   
 *	<scsidev.c>
 *	
 *	SCSI interface (through /dev/sg)
 *   
 *   Copyright (C) 2003 Samuel Rydh (samuel@ibrium.se)
 *   
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License
 *   version 2
 *   
 */

#include "mol_config.h"
#include <scsi/sg.h>
#include <sys/ioctl.h>

#include "scsi-client.h"
#include "async.h"
#include "poll.h"
#include "memory.h"
#include "booter.h"
#include "res_manager.h"
#include "disk.h"

#define SCSI_DEBUG		0

typedef struct ndev {
	scsi_dev_t		*scsi_dev;
	int			async_id;
	int			fd;

	scsi_ureq_t		*queue;
	scsi_ureq_t		**queue_tp;

	struct ndev		*next;
} ndev_t;

typedef struct {
	ndev_t			*dev;
} priv_data_t;

static ndev_t 			*devs;


/************************************************************************/
/*	DEBUG								*/
/************************************************************************/

#if SCSI_DEBUG == 1

#define C_GREEN         "\33[1;32m"
#define C_YELLOW        "\33[1;33m"
#define C_NORMAL        "\33[0;39m"
#define C_RED           "\33[1;31m"

static struct {
	int	cmd;
	char	*name;
} cmd_names[] = {
	{ 0x00,		"TEST UNIT READY" 		},
	{ 0x03,		"REQUEST SENSE" 		},
	{ 0x04,		"FORMAT UNIT" 			},
	{ 0x12,		"INQUIRY" 			},
	{ 0x1B,		"START STOP UNIT"		},
	{ 0x1E,		"P/A MEDIUM REMOVAL"		},
	{ 0x23,		"READ FORMAT CAPACITIES"	},
	{ 0x25,		"READ CAPACITY" 		},
	{ 0x28,		"READ (10)" 			},
	{ 0x2A,		"WRITE (10)" 			},
	{ 0x2B,		"SEEK (10)" 			},
	{ 0x2E,		"WRITE AND VERIFY"		},
	{ 0x2F,		"VERIFY" 			},
	{ 0x35,		"SYNCHRONIZE CACHE"		},
	{ 0x42,		"READ SUB CHANNEL"		},
	{ 0x43,		"READ TOC/PMA/ATIP"		},
	{ 0x45,		"PLAY AUDIO" 			},
	{ 0x46,		"GET CONFIGURATION"		},
	{ 0x47,		"PLAY AUDIO MSF"		},
	{ 0x4A,		"GET EVENT/STATUS NOTIF."	},
	{ 0x4B,		"PAUSE / RESUME"		},
	{ 0x4E,		"STOP PLAY/SCAN"		},
	{ 0x51,		"READ DISC INFO"		},
	{ 0x52,		"READ TRACK INFO"		},
	{ 0x53,		"RESERVE TRACK"			},
	{ 0x54,		"SEND OPC INFO"			},
	{ 0x55,		"MODE SELECT (10)"		},
	{ 0x58,		"REPAIR TRACK"			},
	{ 0x5A,		"MODE SENSE (10)"		},
	{ 0x5B,		"CLOSE TRACK/SESSION"		},
	{ 0x5C,		"READ BUFFER CAPACITY"		},
	{ 0x5D,		"SEND CUE SHEET"		},
	{ 0xA1,		"BLANK"				},
	{ 0xA2,		"SEND EVENT"			},
	{ 0xA3,		"SEND KEY"			},
	{ 0xA4,		"REPORT KEY"			},
	{ 0xA5,		"PLAY AUDIO (12)"		},
	{ 0xA6,		"LOAD/UNLOAD CD/DVD"		},
	{ 0xA7,		"SET READ AHEAD"		},
	{ 0xA8,		"READ (12)"			},
	{ 0xAA,		"WRITE (12)"			},
	{ 0xAC,		"GET PERFORMANCE"		},
	{ 0xAD,		"READ DVD STRUCTURE"		},
	{ 0xB6,		"SET STREAMING"			},
	{ 0xB9,		"READ CD MSF"			},
	{ 0xBA,		"SCAN"				},
	{ 0xBB,		"SET CD SPEED"			},
	{ 0xBD,		"MECHANISM STATUS"		},
	{ 0xBE,		"READ CD"			},
	{ 0xBF,		"SEND DVD STRUCTURE"		},

	/* ATAPI devices might not support these */
	{ 0x1a,		"MODE SENSE (6)"		},
	{ 0x15,		"MODE SELECT (6)"		},
};

static struct {
	int	sense;
	int	critical;
	char	*error;
} sense_tab[] = {
	{ 0x50404, 0,	"Format in progress",				},

	{ 0x20401, 0,	"LU is in progress of becoming ready"		},
	{ 0x20408, 0,	"LU not ready, long write in progress"		},
	{ 0x20404, 0,	"LU not ready, format in progress"		},
	{ 0x23002, 0,	"Cannot read medium - unknown format"		},
	{ 0x23a00, 0,	"Medium not present",				},

	{ 0x52400, 1,	"Invalid field in CDB",				},
	{ 0x52000, 1,	"Invalid Command Operation Code",		},

	{ 0x62800, 0,	"Not ready to ready change, medium may have changed" 	},
	{ 0x62900, 0,  	"Power ON, reset, or bus-device reset occured"	},
};

static void 
dump_cmd_name( scsi_ureq_t *u )
{
	int i;
	for( i=0; i<sizeof(cmd_names)/sizeof(cmd_names[0]); i++ ) {
		if( u->cdb[0] == cmd_names[i].cmd ) {
			printm( C_GREEN "%-20s"C_NORMAL, cmd_names[i].name );
			return;
		}
	}
	printm(C_RED "Unknown SCSI command %02x" C_NORMAL, u->cdb[0] );
}

static void
dump_sg( scsi_ureq_t *u, int count, char *str )
{
	int s, i, j;

	printm("%s [ ", str );
	for( s=0, i=0; i<u->n_sg; i++ ) {
		char *p = u->iovec[i].iov_base;
		for( j=0; s<u->size && s<count && j<u->iovec[i].iov_len; j++, s++ ) {
			printm("%02x ", p[j] );
			if( (s % 20) == 19 )
				printm("\n      ");
		}
	}
	printm("]\n");
}

static void
dump_sense( int sense )
{
	int i;
	for( i=0; i<sizeof(sense_tab)/sizeof(sense_tab[0]); i++ ) {
		if( sense_tab[i].sense == sense ) {
			printm("%s%s\n" C_NORMAL, sense_tab[i].critical ? C_RED : C_NORMAL,
			       sense_tab[i].error );
			return;
		}
	}
	printm( C_RED "Sense %x\n" C_NORMAL, sense );
}

static void
dbg_dump_command( scsi_ureq_t *u, sg_io_hdr_t *io )
{
	int i;

	dump_cmd_name( u );

	printm("%5d [ ", u->size );
	for( i=0; i<u->cdb_len; i++ )
		printm("%02x ", u->cdb[i] );
	while( i++ < 12 )
		printm("-- " );
	printm("]  ");

	if( io->resid  )
		printm("Residue %d\n", io->resid );

	if( io->masked_status && io->masked_status != 1 )
		printm("SCSI-STATUS: %x %x\n", io->status, io->masked_status );

	if( io->msg_status )
		printm("MSG-STATUS: %d\n", io->msg_status );

	if( io->driver_status && (io->driver_status & 0x1f) != 0x8 )
		printm("DRIVER_STATUS %x\n", io->driver_status );

	if( !u->scsi_status )
		printm( C_GREEN "OK\n" C_NORMAL );
	else {
		if( u->sb_actlen >= 14)
			dump_sense( (u->sb[2] & 0xf) << 16 | (u->sb[12] << 8) | u->sb[13] );
		else
			printm( C_RED "[STATUS %x]\n" C_NORMAL, u->scsi_status );
	}
	
	if( !u->scsi_status && u->act_size )
		dump_sg( u, 31, u->is_write ? "OUT" : "IN " );
}

#else
#define	dbg_dump_command(u,v)	
#endif


/************************************************************************/
/*	Engine								*/
/************************************************************************/

static void
unlink_req( ndev_t *dev, scsi_ureq_t *u )
{
	scsi_ureq_t **pp;

	for( pp=&dev->queue; *pp && *pp != u ; pp=&(**pp).next )
		;
	*pp = u->next;
	for( ; *pp ; pp=&(**pp).next )
		;
	dev->queue_tp = pp;
}

static void
async_completion( int fd, int events )
{
	sg_io_hdr_t io;
	scsi_ureq_t *u;
	ndev_t *dev;
	
	if( read(fd, &io, sizeof(io)) < 0 ) {
		perrorm("SCSI read");
		return;
	}
	u = io.usr_ptr;
	dev = ((priv_data_t*)u->pdata)->dev;
	unlink_req( dev, u );

	u->scsi_status = io.masked_status << 1;

	if( (io.driver_status & 0x1f) == 0x8 )
		u->scsi_status = SCSI_STATUS_CHECK_CONDITION;

	if( io.sb_len_wr > 2 ) {
		u->sb_actlen = io.sb_len_wr;
		u->scsi_status = SCSI_STATUS_CHECK_CONDITION;

		/* The Iomega CD burner (USB) returns CHECK_CONDITION but without an
		 * error in the autosense buffer. The returned data is valid though.
		 * We just ignore this condition.
		 */
		if( !u->sb[2] ) {
			printm("Zero SCSI sense ignored (cmd 0x%x)\n", u->cdb[0] );
			u->sb_actlen = u->scsi_status = 0;
		}
	}
	u->act_size = u->size - io.resid;

	dbg_dump_command( u, &io );
	complete_scsi_req( dev->scsi_dev, u );
}

static void
execute( scsi_ureq_t *u, void *refcon )
{
	priv_data_t *pd = (priv_data_t*)u->pdata;
	ndev_t *dev = (ndev_t*)refcon;
	sg_io_hdr_t io;

	pd->dev = dev;

	/* queue to tail */
	u->next = NULL;
	*dev->queue_tp = u;
	dev->queue_tp = &u->next;

	/* issue */
	memset( &io, 0, sizeof(io) );
	io.interface_id = 'S';
	io.dxfer_direction = u->is_write ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
	io.cmd_len = u->cdb_len;
	io.mx_sb_len = sizeof(u->sb);
	io.iovec_count = u->n_sg;
	io.dxfer_len = u->size;
	io.dxferp = (void*)u->iovec;
	io.cmdp = u->cdb;
	io.sbp = u->sb;
	io.timeout = 1000000; 
	/* io.flags = 0; */
	io.usr_ptr = u;

	if( write(dev->fd, &io, sizeof(io)) < 0 ) {
		perrorm("SCSI queue cmd");

		/* FIXME */
		unlink_req( dev, u );
		complete_scsi_req( dev->scsi_dev, u );
		return;
	}
}


/************************************************************************/
/*	init / cleanup / probing					*/
/************************************************************************/

static scsi_ops_t scsi_ops = {
	.execute	= execute,
};

static char *types[16] = {
	"DISK", "TAPE", "PRINTER", "PROCESSOR", "WORM", "CD/DVD", "SCANNER", "OPTICAL", "CHANGER",
	"COMM-LINK", "GARTS_0A", "GARTS_0B", NULL, "ENCLOSURE"
};

static int
probe_device( int n, int auto_ok ) 
{
	int i, fd, match;
	char *p, name[20], buf[128], retbuf[36];
	char inq_cmd[6] = { 0x12, 0, 0, 0, sizeof(retbuf), 0 };
	struct sg_scsi_id id;
	sg_io_hdr_t io;

	sprintf( name, "/dev/sg%d", n );
	if( (fd=open(name, O_RDWR | O_NONBLOCK)) < 0 )
		return -1;

	if( ioctl(fd, SG_GET_SCSI_ID, &id) < 0 ) {
		close( fd );
		return -1;
	}

	/* explicitly exported? */
	sprintf( buf, "%d:%d:%d", id.host_no, id.channel, id.scsi_id );
	for( match=0, i=0; (p=get_str_res_ind("scsi_dev", i, 0)) ; i++ )
		match |= !strcmp( p, buf );

	switch( id.scsi_type ) {
	case 1: /* TAPE */
	case 2: /* PRINTER */
	case 3: /* PROCESSOR */
	case 4: /* WORM */

	case 6: /* SCANNER */
	case 8: /* Changer (jukebox) */
	case 9: /* communication link */
		break;
	case 5: /* CD-ROM, DVD, CD burners etc */
		if( is_classic_boot() && get_str_res("cdboot") ) {
			printm("    SCSI-<%d:%d:%d> ignored (CD-booting)\n",
			       id.host_no, id.channel, id.scsi_id );
			goto bad;
		}
		if( !match && !get_bool_res("generic_scsi_for_cds") ) {
			printm("    Generic SCSI for CD/DVDs disabled\n");
			auto_ok = 0;
		}
		break;
	default:
	case 0: /* DISK */
	case 7: /* OPTICAL (as DISK) */
		auto_ok = 0;
		break;
	}
	if( !match && !auto_ok )
		goto bad;

	/* send INQUIRY */
	memset( &io, 0, sizeof(io) );
	io.interface_id = 'S';
	io.dxfer_direction = SG_DXFER_FROM_DEV;
	io.cmd_len = sizeof(inq_cmd);
	io.cmdp = inq_cmd;
	io.dxfer_len = sizeof(retbuf);
	io.dxferp = retbuf;
	io.timeout = -1;
	if( ioctl(fd, SG_IO, &io) < 0 )
		goto bad;

	p = NULL;
	if( (uint)id.scsi_type < sizeof(types)/sizeof(types[0]) )
		p = types[ id.scsi_type ];
	if( !p ) {
		sprintf( buf, "TYPE-0x%x", id.scsi_type );
		p = buf;
	}
	printm("    %s SCSI-<%d:%d:%d> %s ", name, id.host_no, id.channel, id.scsi_id, p );
	strncpy0( buf, &retbuf[8], 9 );
	printm("%s ", buf);
	strncpy0( buf, &retbuf[16], 17 );
	printm("%s ", buf);
	strncpy0( buf, &retbuf[32], 5 );
	printm("%s\n", buf);

	if( reserve_scsidev(id.host_no, id.channel, id.scsi_id) ) {
		printm("    scsidev already in use!\n");
		goto bad;
	}

	return fd;
bad:
	close( fd );
	return -1;
}


void
scsidev_init( void )
{
	int i, fd, auto_ok = get_bool_res("autoprobe_scsi");
	ndev_t *d;

	printm("\n");
	if( !auto_ok )
		printm("    [SCSI auto-probing disabled]\n\n");

	for( i=0; i<=16; i++ ) {
		if( (fd=probe_device(i, auto_ok)) < 0 )
			continue;

		if( !(d=malloc(sizeof(ndev_t))) )
			break;
		memset( d, 0, sizeof(*d) );
	
		d->fd = fd;
		d->queue_tp = &d->queue;
		d->async_id = add_async_handler( fd, POLLIN | POLLHUP, async_completion, 0 );
		d->scsi_dev = register_scsidev( &scsi_ops, sizeof(priv_data_t), d );

		/* the transform stuff in ide-scsi.c is not needed */
		ioctl( fd, SG_SET_TRANSFORM, 0 );
		//ioctl( fd, SG_SET_TIMEOUT, &timeout );
		
		d->next = devs;
		devs = d;
	}
}

void
scsidev_cleanup( void )
{
	ndev_t *d;
	
	while( (d=devs) ) {
		devs = d->next;

		unregister_scsidev( d->scsi_dev );
		close( d->fd );
		free( d );
	}
}
