/* -*- indent-tabs-mode: t; tab-width: 8; c-basic-offset: 8; -*- */

/* ts A61010 */
/* #include <a ssert.h> */

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

/* #include <m alloc.h>  ts A61013 : not in Linux man 3 malloc */

#include <string.h>
#include <sys/poll.h>
#include <linux/hdreg.h>
#include <stdlib.h>
#include <scsi/sg.h>
#include <scsi/scsi.h>

#include "transport.h"
#include "drive.h"
#include "sg.h"
#include "spc.h"
#include "mmc.h"
#include "sbc.h"
#include "debug.h"
#include "toc.h"
#include "util.h"

/* kludge! glibc headers don't define all the SCSI stuff that we use! */
#ifndef SG_GET_ACCESS_COUNT
#  define SG_GET_ACCESS_COUNT 0x2289
#endif

#include "libdax_msgs.h"
extern struct libdax_msgs *libdax_messenger;

static void enumerate_common(char *fname, int bus_no, int host_no,
			     int channel_no, int target_no, int lun_no);

/* ts A51221 */
int burn_drive_is_banned(char *device_address);

/* ts A60813 : storage objects are in libburn/init.c
   wether to use O_EXCL
   wether to use O_NOBLOCK with open(2) on devices
   wether to take O_EXCL rejection as fatal error */
extern int burn_sg_open_o_excl;
extern int burn_sg_open_o_nonblock;
extern int burn_sg_open_abort_busy;


/* ts A60821
   <<< debug: for tracing calls which might use open drive fds */
int mmc_function_spy(char * text);


static int sgio_test(int fd)
{
	unsigned char test_ops[] = { 0, 0, 0, 0, 0, 0 };
	sg_io_hdr_t s;

	memset(&s, 0, sizeof(sg_io_hdr_t));
	s.interface_id = 'S';
	s.dxfer_direction = SG_DXFER_NONE;
	s.cmd_len = 6;
	s.cmdp = test_ops;
	s.timeout = 12345;
	return ioctl(fd, SG_IO, &s);
}


/* ts A60925 : ticket 74 */
int sg_close_drive_fd(char *fname, int driveno, int *fd, int sorry)
{
	int ret, os_errno, sevno= LIBDAX_MSGS_SEV_DEBUG;
	char msg[4096+100];

	if(*fd < 0)
		return(0);
	ret = close(*fd);
	*fd = -1337;
	if(ret != -1)
		return 1;
	os_errno= errno;

	if (fname != NULL)
		sprintf(msg, "Encountered error when closing drive '%s'",
			fname);
	else
		sprintf(msg, "Encountered error when closing drive");

	if (sorry)
		sevno = LIBDAX_MSGS_SEV_SORRY;
	libdax_msgs_submit(libdax_messenger, driveno, 0x00020002,
			sevno, LIBDAX_MSGS_PRIO_HIGH, msg, os_errno, 0);
	return 0;	
}

int sg_drive_is_open(struct burn_drive * d)
{
	/* a bit more detailed case distinction than needed */
	if (d->fd == -1337)
		return 0;
	if (d->fd < 0)
		return 0;
	return 1;
}

/* ts A60924 */
int sg_handle_busy_device(char *fname, int os_errno)
{
	char msg[4096];

	/* ts A60814 : i saw no way to do this more nicely */ 
	if (burn_sg_open_abort_busy) {
		fprintf(stderr,
	"\nlibburn: FATAL : Application triggered abort on busy device '%s'\n",
			fname);

		/* ts A61007 */
		abort();
		/* a ssert("drive busy" == "non fatal"); */
	}

	/* ts A60924 : now reporting to libdax_msgs */
	sprintf(msg, "Cannot open busy device '%s'", fname);
	libdax_msgs_submit(libdax_messenger, -1, 0x00020001,
			LIBDAX_MSGS_SEV_SORRY, LIBDAX_MSGS_PRIO_LOW,
			msg, os_errno, 0);
	return 1;
}


/* ts A60922 ticket 33 */
/** Returns the next index number and the next enumerated drive address.
    @param idx An opaque handle. Make no own theories about it.
    @param adr Takes the reply
    @param adr_size Gives maximum size of reply including final 0
    @param initialize  1 = start new,
                       0 = continue, use no other values for now
                      -1 = finish
    @return 1 = reply is a valid address , 0 = no further address available
           -1 = severe error (e.g. adr_size too small)
*/
int sg_give_next_adr(burn_drive_enumerator_t *idx,
		     char adr[], int adr_size, int initialize)
{
	/* sg.h : typedef int burn_drive_enumerator_t; */
	static int sg_limit = 32, ata_limit = 26;
	int baseno = 0;

	if (initialize == -1)
		return 0;

	if (initialize  == 1)
		*idx = -1;
	(*idx)++;
	if (*idx >= sg_limit)
		goto next_ata;
	if (adr_size < 10)
		return -1;
	sprintf(adr, "/dev/sg%d", *idx);
	return 1;
next_ata:;
	baseno += sg_limit;
	if (*idx - baseno >= ata_limit)
		goto next_nothing;
	if (adr_size < 9)
		return -1;
	sprintf(adr, "/dev/hd%c", 'a' + (*idx - baseno));
	return 1;
next_nothing:;
	baseno += ata_limit;
	return 0;
}

int sg_is_enumerable_adr(char *adr)
{
	char fname[4096];
	int i, ret = 0, first = 1;

	while (1) {
		ret= sg_give_next_adr(&i, fname, sizeof(fname), first);
		if(ret <= 0)
	break;
		first = 0;
		if (strcmp(adr, fname) == 0)
			return 1;

	}
	return(0);
}


/* ts A60926 */
int sg_release_siblings(int sibling_fds[], int *sibling_count)
{
	int i;
	char msg[81];

	for(i= 0; i < *sibling_count; i++)
		sg_close_drive_fd(NULL, -1, &(sibling_fds[i]), 0);
	if(*sibling_count > 0) {
		sprintf(msg, "Closed %d O_EXCL scsi siblings", *sibling_count);
		libdax_msgs_submit(libdax_messenger, -1, 0x00020007,
			LIBDAX_MSGS_SEV_NOTE, LIBDAX_MSGS_PRIO_HIGH, msg, 0,0);
	}
	*sibling_count = 0;
	return 1;
}


/* ts A60926 */
int sg_open_drive_fd(char *fname, int scan_mode)
{
	int open_mode = O_RDWR, fd;
	char msg[81];

	/* ts A60813 - A60927
	   O_EXCL with devices is a non-POSIX feature
	   of Linux kernels. Possibly introduced 2002.
	   Mentioned in "The Linux SCSI Generic (sg) HOWTO" */
	if(burn_sg_open_o_excl)
		open_mode |= O_EXCL;
	/* ts A60813
	   O_NONBLOCK was already hardcoded in ata_ but not in sg_.
	   There must be some reason for this. So O_NONBLOCK is
	   default mode for both now. Disable on own risk. */
	if(burn_sg_open_o_nonblock)
		open_mode |= O_NONBLOCK;

/* <<< debugging
	fprintf(stderr,
		"\nlibburn: experimental: o_excl= %d , o_nonblock= %d, abort_on_busy= %d\n",
	burn_sg_open_o_excl,burn_sg_open_o_nonblock,burn_sg_open_abort_busy);
	fprintf(stderr,
		"libburn: experimental: O_EXCL= %d , O_NONBLOCK= %d\n",
		!!(open_mode&O_EXCL),!!(open_mode&O_NONBLOCK));
*/
          
	fd = open(fname, open_mode);
	if (fd == -1) {
/* <<< debugging
		fprintf(stderr,
		"\nlibburn: experimental: fname= %s , errno= %d\n",
			fname,errno);
*/
		if (errno == EBUSY) {
			sg_handle_busy_device(fname, errno);
			return -1;
			
		}
		if (scan_mode)
			return -1;
		sprintf(msg, "Failed to open device '%s'",fname);
		libdax_msgs_submit(libdax_messenger, -1, 0x00020005,
				LIBDAX_MSGS_SEV_FATAL, LIBDAX_MSGS_PRIO_HIGH,
				msg, errno, 0);
		return -1;
	}
	return fd;
}


/* ts A60926 */
int sg_open_scsi_siblings(char *path, int driveno,
			  int sibling_fds[], int *sibling_count,
			  int host_no, int channel_no, int id_no, int lun_no)
{
	int tld, i, ret, fd, i_bus_no = -1;
	int i_host_no = -1, i_channel_no = -1, i_target_no = -1, i_lun_no = -1;
	char msg[161], fname[81];

	static char tldev[][81]= {"/dev/sr%d", "/dev/scd%d", "/dev/st%d", ""};

	if(host_no < 0 || id_no < 0 || channel_no < 0 || lun_no < 0)
		return(2);
	if(*sibling_count > 0)
		sg_release_siblings(sibling_fds, sibling_count);
		
	for (tld = 0; tldev[tld][0] != 0; tld++) {
		for (i = 0; i < 32; i++) {
			sprintf(fname, tldev[tld], i);
			ret = sg_obtain_scsi_adr(fname, &i_bus_no, &i_host_no,
				&i_channel_no, &i_target_no, &i_lun_no);
			if (ret <= 0)
		continue;
			if (i_host_no != host_no || i_channel_no != channel_no)
		continue;
			if (i_target_no != id_no || i_lun_no != lun_no)
		continue;

			fd = sg_open_drive_fd(fname, 0);
			if (fd < 0)
				goto failed;

			if (*sibling_count>=LIBBURN_SG_MAX_SIBLINGS) {
				sprintf(msg, "Too many scsi siblings of '%s'",
					path);
				libdax_msgs_submit(libdax_messenger,
					driveno, 0x00020006,
					LIBDAX_MSGS_SEV_FATAL,
					LIBDAX_MSGS_PRIO_HIGH, msg, 0, 0);
				goto failed;
			}
			sprintf(msg, "Opened O_EXCL scsi sibling '%s' of '%s'",
				 fname, path);
			libdax_msgs_submit(libdax_messenger, driveno,
				0x00020004,
				LIBDAX_MSGS_SEV_NOTE, LIBDAX_MSGS_PRIO_HIGH,
				msg, 0, 0);
			sibling_fds[*sibling_count] = fd;
			(*sibling_count)++;
		}
	}
	return(1);
failed:;
	sg_release_siblings(sibling_fds, sibling_count);
	return 0;
}


/* ts A60926 */
int sg_close_drive(struct burn_drive *d)
{
	int ret;

	if (!burn_drive_is_open(d))
		return 0;
	sg_release_siblings(d->sibling_fds, &(d->sibling_count));
	ret = sg_close_drive_fd(d->devname, d->global_index, &(d->fd), 0);
	return ret;
}

void ata_enumerate(void)
{
	struct hd_driveid tm;
	int i, fd;
	char fname[10];

	for (i = 0; i < 26; i++) {
		sprintf(fname, "/dev/hd%c", 'a' + i);
		/* ts A51221 */
		if (burn_drive_is_banned(fname))
	continue;
		fd = sg_open_drive_fd(fname, 1);
		if (fd == -1)
	continue;

		/* found a drive */
		ioctl(fd, HDIO_GET_IDENTITY, &tm);

		/* not atapi */
		if (!(tm.config & 0x8000) || (tm.config & 0x4000)) {
			sg_close_drive_fd(fname, -1, &fd, 0);
	continue;
		}

		/* if SG_IO fails on an atapi device, we should stop trying to 
		   use hd* devices */
		if (sgio_test(fd) == -1) {
			sg_close_drive_fd(fname, -1, &fd, 0);
			return;
		}
		if (sg_close_drive_fd(fname, -1, &fd, 1) <= 0)
	continue;
		enumerate_common(fname, -1, -1, -1, -1, -1);
	}
}

void sg_enumerate(void)
{
	struct sg_scsi_id sid;
	int i, fd, sibling_fds[LIBBURN_SG_MAX_SIBLINGS], sibling_count= 0, ret;
	int bus_no = -1;
	char fname[10];

	for (i = 0; i < 32; i++) {
		sprintf(fname, "/dev/sg%d", i);
		/* ts A51221 */
		if (burn_drive_is_banned(fname))
	continue;
		/* ts A60927 */
		fd = sg_open_drive_fd(fname, 1);
		if (fd == -1)
	continue;

		/* found a drive */
		ioctl(fd, SG_GET_SCSI_ID, &sid);

#ifdef SCSI_IOCTL_GET_BUS_NUMBER
		/* Hearsay A61005 */
		if (ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bus_no) == -1)
			bus_no = -1;
#endif

		if (sg_close_drive_fd(fname, -1, &fd, 
				sid.scsi_type == TYPE_ROM ) <= 0)
	continue;
		if (sid.scsi_type != TYPE_ROM)
	continue;

		/* ts A60927 : trying to do locking with growisofs */
		if(burn_sg_open_o_excl>1) {
			ret = sg_open_scsi_siblings(
					fname, -1, sibling_fds, &sibling_count,
					sid.host_no, sid.channel,
					sid.scsi_id, sid.lun);
			if (ret<=0) {
				sg_handle_busy_device(fname, 0);
	continue;
			}
			/* the final occupation will be done in sg_grab() */
			sg_release_siblings(sibling_fds, &sibling_count);
		}
#ifdef SCSI_IOCTL_GET_BUS_NUMBER
		if(bus_no == -1)
			bus_no = 1000 * (sid.host_no + 1) + sid.channel;
#else
		bus_no = sid.host_no;
#endif
		enumerate_common(fname, bus_no, sid.host_no, sid.channel, 
				 sid.scsi_id, sid.lun);
	}
}

/* ts A60923 - A61005 : introduced new SCSI parameters */
/* ts A61021 : moved non os-specific code to spc,sbc,mmc,drive */
static void enumerate_common(char *fname, int bus_no, int host_no,
			     int channel_no, int target_no, int lun_no)
{
	int ret, i;
	struct burn_drive out;

	/* General libburn drive setup */
	burn_setup_drive(&out, fname);

	/* This transport adapter uses SCSI-family commands and models
           (seems the adapter would know better than its boss, if ever) */
	ret = burn_scsi_setup_drive(&out, bus_no, host_no, channel_no,
				    target_no, lun_no, 0);
	if (ret<=0)
		return;

	/* Operating system adapter is Linux Generic SCSI (sg) */
	/* Adapter specific handles and data */
	out.fd = -1337;
	out.sibling_count = 0;
	for(i= 0; i<LIBBURN_SG_MAX_SIBLINGS; i++)
		out.sibling_fds[i] = -1337;
	/* Adapter specific functions */
	out.grab = sg_grab;
	out.release = sg_release;
	out.drive_is_open= sg_drive_is_open;
	out.issue_command = sg_issue_command;

	/* Finally register drive and inquire drive information */
	burn_drive_finish_enum(&out);
}

/*
	we use the sg reference count to decide whether we can use the
	drive or not.
	if refcount is not one, drive is open somewhere else.

	ts A60813: this test is too late. O_EXCL is the stronger solution.
	After all the test was disabled already in icculus.org/burn CVS.
*/
int sg_grab(struct burn_drive *d)
{
	int fd, count, os_errno= 0, ret;

	/* ts A60813 */
	int open_mode = O_RDWR;

/* ts A60821
   <<< debug: for tracing calls which might use open drive fds */
	mmc_function_spy("sg_grab");


	/* ts A60813 - A60927
	   O_EXCL with devices is a non-POSIX feature
	   of Linux kernels. Possibly introduced 2002.
	   Mentioned in "The Linux SCSI Generic (sg) HOWTO".
	*/
	if(burn_sg_open_o_excl)
		open_mode |= O_EXCL;

	/* ts A60813
	   O_NONBLOCK was hardcoded here. So it should stay default mode. */
	if(burn_sg_open_o_nonblock)
		open_mode |= O_NONBLOCK;

	/* ts A60813
	   After enumeration the drive fd is probably still open.
	   -1337 is the initial value of burn_drive.fd and the value after
	   relase of drive. Unclear why not the official error return
	   value -1 of open(2) war used. */
				/* ts A60822: was  if(d->fd == -1337) { */
	if(! burn_drive_is_open(d)) {

		/* ts A60821
   		<<< debug: for tracing calls which might use open drive fds */
		mmc_function_spy("sg_grab ----------- opening");

		/* ts A60926 */
		if(burn_sg_open_o_excl>1) {
			fd = -1;
			ret = sg_open_scsi_siblings(d->devname,
					d->global_index,d->sibling_fds,
					&(d->sibling_count),
					d->host, d->channel, d->id, d->lun);
			if(ret <= 0)
				goto drive_is_in_use;
		}

		fd = open(d->devname, open_mode);
		os_errno = errno;
	} else
		fd= d->fd;

	/* ts A61007 : this is redundant */
	/* a ssert(fd != -1337); */
	
	if (fd >= 0) {

		/* ts A60814:
		   according to my experiments this test would work now ! */

		/* ts A60926 : this was disabled */
		/* Tests with growisofs on kernel 2.4.21 yielded that this
		   does not help against blocking on busy drives.
		*/
/* <<< the old dummy */
/*              er = ioctl(fd, SG_GET_ACCESS_COUNT, &count);*/
		count = 1;

		if (1 == count) {
			d->fd = fd;
			fcntl(fd, F_SETOWN, getpid());
			d->released = 0;
			return 1;
		}

drive_is_in_use:;
		libdax_msgs_submit(libdax_messenger, d->global_index,
			0x00020003,
			LIBDAX_MSGS_SEV_SORRY, LIBDAX_MSGS_PRIO_HIGH,
			"Could not grab drive - already in use", 0, 0);
		sg_close_drive(d);
		d->fd = -1337;
		return 0;
	}
	libdax_msgs_submit(libdax_messenger, d->global_index, 0x00020003,
			LIBDAX_MSGS_SEV_SORRY, LIBDAX_MSGS_PRIO_HIGH,
			"Could not grab drive", os_errno, 0);
	return 0;
}

/*
	non zero return means you still have the drive and it's not
	in a state to be released? (is that even possible?)
*/

int sg_release(struct burn_drive *d)
{
	/* ts A60821
   	<<< debug: for tracing calls which might use open drive fds */
	mmc_function_spy("sg_release");

	if (d->fd < 1) {
		burn_print(1, "release an ungrabbed drive.  die\n");
		return 0;
	}

	/* ts A60821
   	<<< debug: for tracing calls which might use open drive fds */
	mmc_function_spy("sg_release ----------- closing");

	sg_close_drive(d);
	return 0;
}

int sg_issue_command(struct burn_drive *d, struct command *c)
{
	int done = 0, no_c_page = 0;
	int err;
	sg_io_hdr_t s;

/* ts A60821
   <<< debug: for tracing calls which might use open drive fds */
	char buf[161];
	sprintf(buf,"sg_issue_command   d->fd= %d  d->released= %d\n",
		d->fd,d->released);
	mmc_function_spy(buf);

	/* ts A61010 : with no fd there is no chance to send an ioctl */
	if (d->fd < 0) {
		c->error = 1;
		return 0;
	}

	c->error = 0;
	memset(&s, 0, sizeof(sg_io_hdr_t));

	s.interface_id = 'S';

	if (c->dir == TO_DRIVE)
		s.dxfer_direction = SG_DXFER_TO_DEV;
	else if (c->dir == FROM_DRIVE)
		s.dxfer_direction = SG_DXFER_FROM_DEV;
	else if (c->dir == NO_TRANSFER) {
		s.dxfer_direction = SG_DXFER_NONE;

		/* ts A61007 */
		/* a ssert(!c->page); */
		no_c_page = 1;
	}
	s.cmd_len = c->oplen;
	s.cmdp = c->opcode;
	s.mx_sb_len = 32;
	s.sbp = c->sense;
	memset(c->sense, 0, sizeof(c->sense));
	s.timeout = 200000;
	if (c->page && !no_c_page) {
		s.dxferp = c->page->data;
		if (c->dir == FROM_DRIVE) {
			s.dxfer_len = BUFFER_SIZE;
/* touch page so we can use valgrind */
			memset(c->page->data, 0, BUFFER_SIZE);
		} else {

			/* ts A61010 */
			/* a ssert(c->page->bytes > 0); */
			if (c->page->bytes <= 0) {
				c->error = 1;
				return 0;
			}

			s.dxfer_len = c->page->bytes;
		}
	} else {
		s.dxferp = NULL;
		s.dxfer_len = 0;
	}
	s.usr_ptr = c;

	do {
		err = ioctl(d->fd, SG_IO, &s);

		/* ts A61010 */
		/* a ssert(err != -1); */
		if (err == -1) {
			libdax_msgs_submit(libdax_messenger,
				 d->global_index, 0x0002010c,
				 LIBDAX_MSGS_SEV_FATAL, LIBDAX_MSGS_PRIO_HIGH,
				 "Failed to transfer command to drive",
				 errno, 0);
			sg_close_drive(d);
			d->released = 1;
			d->busy = BURN_DRIVE_IDLE;
			c->error = 1;
			return -1;
		}

		if (s.sb_len_wr) {
			if (!c->retry) {
				c->error = 1;
				return 1;
			}
			switch (scsi_error(d, s.sbp, s.sb_len_wr)) {
			case RETRY:
				done = 0;
				break;
			case FAIL:
				done = 1;
				c->error = 1;
				break;
			}
		} else {
			done = 1;
		}
	} while (!done);
	return 1;
}

enum response scsi_error(struct burn_drive *d, unsigned char *sense,
			 int senselen)
{
	int key, asc, ascq;

	senselen = senselen;
	key = sense[2];
	asc = sense[12];
	ascq = sense[13];

	burn_print(12, "CONDITION: 0x%x 0x%x 0x%x on %s %s\n",
		   key, asc, ascq, d->idata->vendor, d->idata->product);

	switch (asc) {
	case 0:
		burn_print(12, "NO ERROR!\n");
		return RETRY;

	case 2:
		burn_print(1, "not ready\n");
		return RETRY;
	case 4:
		burn_print(1,
			   "logical unit is in the process of becoming ready\n");
		return RETRY;
	case 0x20:
		if (key == 5)
			burn_print(1, "bad opcode\n");
		return FAIL;
	case 0x21:
		burn_print(1, "invalid address or something\n");
		return FAIL;
	case 0x24:
		if (key == 5)
			burn_print(1, "invalid field in cdb\n");
		else
			break;
		return FAIL;
	case 0x26:
		if ( key == 5 )
			burn_print( 1, "invalid field in parameter list\n" );
		return FAIL;
	case 0x28:
		if (key == 6)
			burn_print(1,
				   "Not ready to ready change, medium may have changed\n");
		else
			break;
		return RETRY;
	case 0x3A:
		burn_print(12, "Medium not present in %s %s\n",
			   d->idata->vendor, d->idata->product);

		d->status = BURN_DISC_EMPTY;
		return FAIL;
	}
	burn_print(1, "unknown failure\n");
	burn_print(1, "key:0x%x, asc:0x%x, ascq:0x%x\n", key, asc, ascq);
	return FAIL;
}

/* ts A60922 */
/** Try to obtain SCSI address parameters.
    @return  1 is success , 0 is failure
*/
int sg_obtain_scsi_adr(char *path, int *bus_no, int *host_no, int *channel_no,
                       int *target_no, int *lun_no)
{
	int fd, ret;
	struct my_scsi_idlun {
		int x;
		int host_unique_id;
	};
 	struct my_scsi_idlun idlun;

	if (strncmp(path, "/dev/hd", 7) == 0 
	    && path[7] >= 'a' && path[7] <= 'z' && path[8] == 0)
		return 0; /* on RIP 14 all hdx return SCSI adr 0,0,0,0 */

	fd = open(path, O_RDONLY | O_NONBLOCK);
	if(fd < 0)
		return 0;

#ifdef SCSI_IOCTL_GET_BUS_NUMBER
	/* Hearsay A61005 */
	if (ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, bus_no) == -1)
		*bus_no = -1;
#endif

	/* http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/scsi_g_idlun.html */
	ret = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &idlun);

	sg_close_drive_fd(path, -1, &fd, 0);
	if (ret == -1)
		return(0);
	*host_no= (idlun.x>>24)&255;
	*channel_no= (idlun.x>>16)&255;
	*target_no= (idlun.x)&255;
	*lun_no= (idlun.x>>8)&255;
#ifdef SCSI_IOCTL_GET_BUS_NUMBER
	if(*bus_no == -1)
		*bus_no = 1000 * (*host_no + 1) + *channel_no;
#else
	*bus_no= *host_no;
#endif
	return 1;
}
