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

#include <assert.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 <malloc.h>
#include <string.h>
#include <sys/poll.h>
#include <linux/hdreg.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"

static void enumerate_common(char *fname);

/* 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);
}

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

	/* ts A60813 */
	int open_mode = O_RDWR;

	/* ts A60813
	   O_EXCL with block devices is an unpublished feature
	   of Linux kernels. Possibly introduced 2002.
	   It can only be used if libburn stops opening several
	   file descriptor on the same block device.
	   See comment in sg_grab() */
	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;

	for (i = 0; i < 26; i++) {
		sprintf(fname, "/dev/hd%c", 'a' + i);
		/* open O_RDWR so we don't think read only drives are
		   in some way useful
		 */
		/* ts A51221 */
		if (burn_drive_is_banned(fname))
			continue;
		fd = open(fname, open_mode);
		if (fd == -1) {
/* <<< debugging
			fprintf(stderr,
			"\nlibburn: experimental: fname= %s , errno= %d\n",
				fname,errno);
*/
			/* ts A60814 : i see no way to do this more nicely */ 
			if (errno == EBUSY && burn_sg_open_abort_busy) {
				fprintf(stderr,
	"\nlibburn: FATAL : Application triggered abort on busy drive '%s'\n",
					fname);
				/* <<< maybe one should plainly exit here */
				assert("drive busy" == "non fatal");
			}
			continue;
		}
		/* found a drive */
		ioctl(fd, HDIO_GET_IDENTITY, &tm);

		/* not atapi */
		if (!(tm.config & 0x8000) || (tm.config & 0x4000)) {
			close(fd);
			continue;
		}

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

void sg_enumerate(void)
{
	struct sg_scsi_id sid;
	int i, fd;
	char fname[10];

	/* ts A60813 */
	int open_mode = O_RDWR;

	/* ts A60813
	   O_EXCL with block devices is an unpublished feature
	   of Linux kernels. Possibly introduced 2002.
	   It can only be used if libburn stops opening several
	   file descriptor on the same block device.
	   See comment in sg_grab() */
	if(burn_sg_open_o_excl)
		open_mode |= O_EXCL;
	/* ts A60813
	   O_NONBLOCK was not hardcoded in sg_ but was in ata_.
	   I myself test mainly sg_ and it seems to be ok with 
	   O_NONBLOCK. So it should stay default mode. */
	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));
          
*/

	for (i = 0; i < 32; i++) {
		sprintf(fname, "/dev/sg%d", i);
		/* open RDWR so we don't accidentally think read only drives
		   are in some way useful
		 */
		/* ts A51221 */
		if (burn_drive_is_banned(fname))
			continue;
		fd = open(fname, open_mode);

		if (fd == -1) {
/* <<< debugging
			fprintf(stderr,
			"\n cdrskin: experimental: fname= %s , errno= %d\n",
				fname,errno);
*/
			/* ts A60814 : i see no way to do this more nicely */ 
			if (errno == EBUSY && burn_sg_open_abort_busy) {
				fprintf(stderr,
	"\nlibburn: FATAL : Application triggered abort on busy drive '%s'\n",
					fname);
				/* <<< maybe one should plainly exit here */
				assert("drive busy" == "non fatal");
			}
			continue;
		}
		/* found a drive */
		ioctl(fd, SG_GET_SCSI_ID, &sid);
		close(fd);
		if (sid.scsi_type != TYPE_ROM)
			continue;

		enumerate_common(fname);
	}
}

static void enumerate_common(char *fname)
{
	struct burn_drive *t;
	struct burn_drive out;

	out.devname = burn_strdup(fname);
	out.fd = -1337;

	out.grab = sg_grab;
	out.release = sg_release;
	out.issue_command = sg_issue_command;
	out.getcaps = spc_getcaps;
	out.released = 1;
	out.status = BURN_DISC_UNREADY;

	out.eject = sbc_eject;
	out.load = sbc_load;
	out.lock = spc_prevent;
	out.unlock = spc_allow;
	out.read_disc_info = spc_sense_write_params;
	out.get_erase_progress = spc_get_erase_progress;
	out.test_unit_ready = spc_test_unit_ready;
	out.probe_write_modes = spc_probe_write_modes;
	out.read_toc = mmc_read_toc;
	out.write = mmc_write;
	out.erase = mmc_erase;
	out.read_sectors = mmc_read_sectors;
	out.perform_opc = mmc_perform_opc;
	out.set_speed = mmc_set_speed;
	out.send_parameters = spc_select_error_params;
	out.send_write_parameters = spc_select_write_params;
	out.send_cue_sheet = mmc_send_cue_sheet;
	out.sync_cache = mmc_sync_cache;
	out.get_nwa = mmc_get_nwa;
	out.close_disc = mmc_close_disc;
	out.close_session = mmc_close_session;
	out.idata = malloc(sizeof(struct scsi_inquiry_data));
	out.idata->valid = 0;
	out.mdata = malloc(sizeof(struct scsi_mode_data));
	out.mdata->valid = 0;
	memset(&out.params, 0, sizeof(struct params));
	t = burn_drive_register(&out);

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

/* try to get the drive info */
	if (sg_grab(t)) {
		burn_print(2, "getting drive info\n");
		t->getcaps(t);
		t->unlock(t);
		t->released = 1;
	} else {
		burn_print(2, "unable to grab new located drive\n");
	}

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

}

/*
	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 diabled already in icculus.org/burn CVS.
*/
int sg_grab(struct burn_drive *d)
{
	int fd, count;

	/* 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
	   O_EXCL with block devices is an unpublished feature
	   of Linux kernels. Possibly introduced 2002.
	   It can only be used if libburn stops opening several
	   file descriptor on the same block device.
	   See comment below */
	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");

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

	assert(fd != -1337);
	if (-1 != fd) {

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

/*		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;
		}
		burn_print(1, "could not acquire drive - already open\n");
		close(fd);
		return 0;
	}
	burn_print(1, "could not acquire drive\n");
	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");

	close(d->fd);
	d->fd = -1337;
	return 0;
}

int sg_issue_command(struct burn_drive *d, struct command *c)
{
	int done = 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);


	c->error = 0;
/*
this is valid during the mode probe in scan
	if (d->fd < 1 || d->released) {
		burn_print(1,
			      "command issued on ungrabbed drive, chaos.\n");
		burn_print(1, "fd = %d, released = %d\n", d->fd,
			      d->released);
	}
*/
	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;
		assert(!c->page);
	}
	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) {
		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 {
			assert(c->page->bytes > 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);
		assert(err != -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;
}
