#define _LARGEFILE_SOURCE 
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64
#if defined(__linux)
/* ... and "engage" glibc large file support */
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

#include "transport.hxx"

static int	media_written=0,next_track=1,velocity=0;
extern int	media_fd,dvd_compat,speed_factor;
extern char	*media_device;

extern "C"
int flush_cache (int fd)
{ Scsi_Command cmd(fd);

    cmd[0] = 0x35;		// FLUSH CACHE
    cmd[1] = 0x02;		// "IMMED"
    cmd[9] = 0;
    if (cmd.transport ()) return 1;

  return wait_for_unit (cmd,NULL);
}

extern "C"
int get_mmc_profile (int fd)
{ Scsi_Command cmd(fd);
  unsigned char buf[8];

    cmd[0] = 0x46;
    cmd[8] = sizeof(buf);
    cmd[9] = 0;
    if (cmd.transport(READ,buf,sizeof(buf)))
	perror (":-( unable to GET CONFIGURATION"),
	exit (1);

  return buf[6]<<8|buf[7];
}

extern "C"
off64_t get_capacity (int fd,int mmc_profile)
{ Scsi_Command cmd(fd);
  unsigned char buf[32];
  off64_t ret;
  struct stat64 sb;
  unsigned int nwa,free_blocks;

    if (mmc_profile==-1 || fstat64 (fd,&sb) || S_ISREG(sb.st_mode))
	return 0;

    switch (mmc_profile)
    {	case 0x1A:
    	case 0x13:
	    cmd[0] = 0x23;	// READ FORMAT CAPACITIES
	    cmd[8] = 12;
	    cmd[9] = 0;
	    if (cmd.transport (READ,buf,12))
	    {	perror (":-( unanble to READ FORMAT CAPACITIES");
		return 0;
	    }

	    ret  = buf[4]<<24;
	    ret |= buf[5]<<16;
	    ret |= buf[6]<<8;
	    ret |= buf[7];
	    ret *= buf[9]<<16|buf[10]<<8|buf[11];
	    break;

	case 0x1B:
	case 0x11:
	case 0x14:
	    cmd[0] = 0x52;	// READ TRACK INFORMATION
	    cmd[1] = 1;
	    cmd[4] = next_track>>8;
	    cmd[5] = next_track;	// last track, set up earlier
	    cmd[8] = sizeof(buf);
	    cmd[9] = 0;
	    if (cmd.transport (READ,buf,sizeof(buf)))
	    {	perror (":-( unable to READ TRACK INFORMATION");
		return 0;
	    }

	    nwa = 0;
	    if (buf[7]&1)	// NWA_V
	    {	nwa  = buf[12]<<24;
		nwa |= buf[13]<<16;
		nwa |= buf[14]<<8;
		nwa |= buf[15];
	    }
	    free_blocks  = buf[16]<<24;
	    free_blocks |= buf[17]<<16;
	    free_blocks |= buf[18]<<8;
	    free_blocks |= buf[19];
	    ret = nwa + free_blocks;
	    ret *= 2048;
	    break;

	default:
	    ret = 0;
	    break;
    }

  return ret;
}

extern "C"
ssize_t poor_mans_pwrite64 (int fd,const void *buff,size_t size,off64_t foff)
{ Scsi_Command cmd(fd);
  unsigned char *sense=cmd.sense(),bcap[12];
  unsigned int lba,bsize,bfree;
  int msecs,retried=0;

    if (foff&0x7FFF || size&0x7FFF)	// 32K block size
	return -1;

    lba = foff>>11;

    while (1)
    {	cmd[0] = 0x2A;			// WRITE(10)
	cmd[2] = (lba>>24)&0xff;	// Logical Block Addrss
	cmd[3] = (lba>>16)&0xff;
	cmd[4] = (lba>>8)&0xff;
	cmd[5] = lba&0xff;
	cmd[7] = (size>>19)&0xff;
	cmd[8] = (size>>11)&0xff;
	cmd[9] = 0;
	if (!cmd.transport (WRITE,(void *)buff,size))
	    break;

	//--- WRITE failed ---//
	if (!retried && velocity!=0 && sense[12]==0x4 && sense[13]==0x8)
	{   // "LONG WRITE IN PROGRESS"
	    // Is it only Pioneer DVD-RW units that needs this?
	    cmd[0] = 0x5C;		// READ BUFFER CAPACITY
	    cmd[8] = sizeof(bcap);
	    cmd[9] = 0;
	    if (cmd.transport (READ,bcap,sizeof(bcap)))
		bfree=0, bsize=128*1024; // bogus values...
	    else
	    {	bsize = bcap[4]<<24|bcap[5]<<16|bcap[6]<<8|bcap[7];
		bfree = bcap[8]<<24|bcap[9]<<16|bcap[10]<<8|bcap[11];
		bsize /= 4;	// we drain 1/4 of the buffer
	    }
	    if (bsize < 128*1024) bsize = 128*1024;
	    if (bfree < bsize)
	    {	msecs = (bsize-bfree)*1000;
		msecs /= velocity;
		poll (NULL,0,msecs);
		retried=1;
		continue;
	    }
	    lba = bfree|0x80000000; // signal insane bfree...
	}
	fprintf (stderr,":-[ LBA=%xh, SENSE KEY=%xh/ASC=%xh/ASCQ=%xh ]\n",
			lba,sense[2]&0x0F,sense[12],sense[13]);
	if (lba==0 && sense[12]==0x64)
	    fprintf (stderr,":-[ attempt -blank=full ]\n");

	return -1;
    }

    media_written = 1;

  return size;
}

extern "C"
int plus_rw_restart_format (int fd)
{ Scsi_Command cmd(fd);
  unsigned char descr[12];

    memset (descr,0,sizeof(descr));
    descr[1]=0x02;		// "IMMED" flag
    descr[3]=0x08;		// "Descriptor Length" (LSB)
    descr[8]=0x98;		// "Format type" 0x26
    descr[11]=1;		// "Restart Format"

    cmd[0] = 0x04;		// FORMAT UNIT
    cmd[1] = 0x11;		// "FmtData" and "Format Code"
    cmd[5] = 0;
    if (cmd.transport(WRITE,descr,sizeof(descr)))
    {	perror (":-( unable to RESTART FORMAT");
	return 1;
    }

    wait_for_unit(cmd,NULL);

  return 0;
}

extern "C"
void plus_rw_finalize ()
{   while (media_written)
    { Scsi_Command cmd(media_fd);

	fprintf (stderr,"%s: flushing cache\n",media_device);
	cmd[0] = 0x35;		// FLUSH CACHE
	cmd[1] = 0x02;		// "IMMED"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to FLUSH CACHE");

	if (wait_for_unit (cmd,NULL)) break;

	if (!dvd_compat)
	{   fprintf (stderr,"%s: stopping de-icing\n",media_device);
	    cmd[0] = 0x5B;	// CLOSE TRACK/SESSION
	    cmd[1] = 0x01;	// "IMMED"
	    cmd[2] = 0;		// "Stop De-Icing"
	    cmd[9] = 0;
	    if (cmd.transport())
		perror (":-( unable to CLOSE SESSION");

	    if (wait_for_unit (cmd,NULL)) break;
	}

	fprintf (stderr,"%s: writing lead-out\n",media_device);
	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = 0x02;		// "Close session"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE SESSION");

	if (wait_for_unit (cmd,NULL)) break;

	media_written = 0;
    }
    _exit (errno);
}

extern "C"
void plus_r_finalize ()
{   while (media_written)
    { Scsi_Command  cmd(media_fd);
      unsigned char *page05=NULL,*p,header[8];
      int           mode,len;

	fprintf (stderr,"%s: flushing cache\n",media_device);
	cmd[0] = 0x35;		// FLUSH CACHE
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to FLUSH CACHE");

	if (wait_for_unit (cmd,NULL)) break;

	//
	// Even though MMC-4 draft explicitely exempts DVD+RW/+R media
	// from those being affected by Page 05h settings, some
	// firmwares apparently pay attention to Multi-session flag
	// when finalizing DVD+R media. Well, we probably can't blame
	// vendors as specification is still a work in progress not to
	// mention that it was published after DVD+R was introduced to
	// the market.
	//
	do
	{   cmd[0] = 0x5A;
	    cmd[2] = 0x05;
	    cmd[8] = sizeof(header);
	    cmd[9] = 0;
	    if (cmd.transport(READ,header,sizeof(header)))	break;

	    len = (header[0]<<8|header[1])+2;
	    page05 = (unsigned char *)malloc(len);
	    if (page05 == NULL)				break;

	    cmd[0] = 0x5A;
	    cmd[2] = 0x05;
	    cmd[7] = len>>8;
	    cmd[8] = len;
	    cmd[9] = 0;
	    if (cmd.transport(READ,page05,len))	break;
	    p = page05 + 8 + (page05[6]<<8|page05[7]);
	    if ((p[0]&0x3F) != 0x05)			break;
	    if ((p[1]+8+2) > len)			break;

	    p[0] &= 0x7F;
	    p[2] &= 0xF0, p[2] |= 1;		// TAO
	    if (dvd_compat)	p[3] &= 0x3F;	// Single-session
	    else		p[3] |= 0xC0;	// Multi-session

	    len = p[1]+8+2, memset (p-8,0,8);

	    cmd[0] = 0x55;
	    cmd[1] = 0x10;
	    cmd[7] = len>>8;
	    cmd[8] = len;
	    cmd[9] = 0;
	    cmd.transport(WRITE,p-8,len);
	} while (0);

	if (page05 != NULL) free (page05);

	fprintf (stderr,"%s: closing track\n",media_device);
	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = 0x01;		// "Close Track"
	cmd[4] = next_track>>8;
	cmd[5] = next_track;
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE SESSION");

	if (wait_for_unit (cmd,NULL)) break;

	if (dvd_compat)
	    mode=0x06, fprintf (stderr,"%s: closing disk\n",media_device);
	else
	    mode=0x02, fprintf (stderr,"%s: closing session\n",media_device);

	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = mode;		// "Close session"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE SESSION");

	if (wait_for_unit (cmd,NULL)) break;

	media_written = 0;
    }
    _exit (errno);
}

extern "C"
int plusminus_r_C_parm (int fd,char *C_parm)
{ unsigned int next_session, prev_session;
  Scsi_Command cmd(fd);
  unsigned char *sense=cmd.sense(), buf[36];

    cmd[0] = 0x51;		// READ DISC INFORMATION
    cmd[8] = 32;
    cmd[9] = 0;
    if (cmd.transport (READ,buf,32))
	perror (":-( unable to READ DISC INFORMATION"),
	exit(1);

    if ((buf[2]&3) != 1)
	fprintf (stderr,":-( disc is not appendable\n"), exit(1);

    if (((buf[2]>>2)&3) != 0)
	fprintf (stderr,":-( last session is not empty\n"), exit(1);

    next_track = buf[5]|buf[10]<<8;

    cmd[0] = 0x52;		// READ TRACK INFORMATION
    cmd[1] = 1;
    cmd[4] = next_track>>8;
    cmd[5] = next_track;	// ask for last track
    cmd[8] = sizeof(buf);
    cmd[9] = 0;
    if (cmd.transport (READ,buf,sizeof(buf)))
    {	perror (":-( unable to READ TRACK INFORMATION");
	if (sense[12]==0x24) // hp dvd200i returns 24 if media is full
	    fprintf (stderr,":-( media must be full already\n");
	exit (1);
    }

    if (buf[7]&1)		// NWA_V
    {	next_session  = buf[12]<<24;
	next_session |= buf[13]<<16;
	next_session |= buf[14]<<8;
	next_session |= buf[15];
    }
    else
	fprintf (stderr,":-( Next Writable Address is invalid\n"),
	exit(1);

    //
    // All manuals say the data is fabricated, presumably implying
    // that one should use another command. But we stick to this one
    // because kernel uses this very command to mount multi-session
    // discs.
    //
    cmd[0] = 0x43;		// READ TOC
    cmd[2] = 1;			// "Session info"
    cmd[8] = 12;
    cmd[9] = 0;
    if (cmd.transport (READ,buf,12))
	perror (":-( unable to READ SESSION INFO"),
	exit (1);

    prev_session  = buf[8]<<24;
    prev_session |= buf[9]<<16;
    prev_session |= buf[10]<<8;
    prev_session |= buf[11];

    sprintf (C_parm,"%d,%d",prev_session+16,next_session);

  return next_session;
}

extern "C"
int minus_rw_setup (int fd)
{ Scsi_Command cmd(fd);
  unsigned int len;
  unsigned short profile;
  unsigned char *sense=cmd.sense(),
		header[8],feature21[24],track[32],*p;
  class autofree {
    private:
	unsigned char *ptr;
    public:
	autofree()			{ ptr=NULL; }
	~autofree()			{ if (ptr) free(ptr); }
	unsigned char *operator=(unsigned char *str)
					{ return ptr=str; }
	operator unsigned char *()	{ return ptr; }
  } page05, page2A;

    cmd[0] = 0x46;	// GET CONFIGURATION
    cmd[1] = 2;		// ask for the only feature...
    cmd[3] = 0x21;	// the "Incremental Streaming Writable" one
    cmd[8] = 8;		// read the header only to start with
    cmd[9] = 0;
    if (cmd.transport(READ,feature21,8))
	perror (":-( unable to GET CONFIGURATION"), exit(1);

    len = feature21[0]<<24|feature21[1]<<16|feature21[2]<<8|feature21[3];
    len += 4;
    if (len>sizeof(feature21))
	len = sizeof(feature21);
    else if (len<(8+8))
	fprintf (stderr,":-( READ FEATURE DESCRIPTOR 0021h: insane length\n"),
	exit(1);

    cmd[0] = 0x46;	// GET CONFIGURATION
    cmd[1] = 2;		// ask for the only feature...
    cmd[3] = 0x21;	// the "Incremental Streaming Writable" one
    cmd[8] = len;	// this time with real length
    cmd[9] = 0;
    if (cmd.transport(READ,feature21,len))
	perror (":-( unable to READ FEATURE DESCRIPTOR 0021h"), exit(1);

    if ((feature21[8+1]&1)==0)
	fprintf (stderr,":-( FEATURE 0021h is not in effect\n"),
	exit(1);

    cmd[0] = 0x52;	// READ TRACK INFORMATION
    cmd[1] = 1;		// track
    cmd[5] = 1;		// #1 assuming that all tracks are in same mode
    cmd[8] = sizeof(track);
    cmd[9] = 0;
    if (cmd.transport(READ,track,sizeof(track)))
	perror (":-( unable to READ TRACK INFORMATION"), exit(1);

    // WRITE PAGE SETUP //
    cmd[0] = 0x5A;	// MODE SENSE
    cmd[2] = 0x05;	// "Write Page"
    cmd[8] = sizeof(header);	// header only to start with
    cmd[9] = 0;
    if (cmd.transport(READ,header,sizeof(header)))
	perror (":-( unable to MODE SENSE"), exit(1);

    len = (header[0]<<8|header[1])+2;
    page05 = (unsigned char *)malloc(len);
    if (page05 == NULL)
	fprintf (stderr,":-( memory exhausted\n"), exit(1);

    cmd[0] = 0x5A;	// MODE SENSE
    cmd[2] = 0x05;	// "Write Page"
    cmd[7] = len>>8;
    cmd[8] = len;	// real length this time
    cmd[9] = 0;
    if (cmd.transport(READ,page05,len))
	fprintf (stderr,":-[ MODE SENSE failed with "
			"SK=%xh/ASC=%02xh/ASCQ=%02xh\n",
			sense[2]&0xF,sense[12],sense[13]),
	exit(1);

    p = page05 + 8 + (page05[6]<<8|page05[7]);
    memset (p-8,0,8);
    p[0] &= 0x7F;
    p[2] &= 0xF0;	// Incremental recording
    p[2] |= 0x40;	// insist on BUFE on
    p[2] &= ~0x10;	// no Test Write

    // setup Preferred Link Size
#if 0
    // At least Pioneer DVR-104 return some bogus data in
    // Preferred Link Size...
    if (feature21[8+7])	p[2] |= 0x20,  p[5] = feature21[8+8];
    else		p[2] &= ~0x20, p[5] = 0;
#else
    p[2] |= 0x20, p[5] = 0x10;
#endif

    // setup Track Mode
    p[3] &= 0xF0, p[3] |= track[5]&0x0F;

    // setup Data Block Type
    if ((track[6]&0x0F)==1)	p[4] = 8;
    else	fprintf (stderr,":-( none Mode 1 track\n"), exit(1);

    switch (profile = (feature21[6]<<8|feature21[7]))
    {	case 0x0013:	// DVD-RW Restricted Overwrite
	    p[3] &= 0x3F;	// Single-session?
	    // setup Fixed Packet Size
	    if (track[6]&0x10)
		p[3] |= 0x20, memcpy (p+10,track+20,4);
	    else
		fprintf (stderr,":-( track is not formatted for fixed packet size\n"),
		exit(1);
	    break;
	case 0x0011:	// DVD-R  Sequential Recording
	case 0x0014:	// DVD-RW Sequential Recording
	    if (dvd_compat)	p[3] &= 0x3F;	// Single-session
	    else		p[3] |= 0xC0;	// Multi-session
	    // setup for Variable Packet Size
	    if (track[6]&0x10)
		fprintf (stderr,":-( track is formatted for fixed packet size\n"),
		exit(1);
	    else
		p[3] &= ~0x20, memset (p+10,0,4);
	    break;
	default:
	    fprintf (stderr,":-( invalid profile %04xh\n",profile);
	    exit(1);
	    break;
    }

    len = p[1]+8+2;

    cmd[0] = 0x55;	// MODE SELECT
    cmd[1] = 0x10;	// conformant
    cmd[7] = len>>8;
    cmd[8] = len;
    cmd[9] = 0;
    if (cmd.transport(WRITE,p-8,len))
	fprintf (stderr,":-[ MODE SELECT failed with "
			"SK=%xh/ASC=%02xh/ASCQ=%02xh\n",
			sense[2]&0xF,sense[12],sense[13]),
	exit(1);
    // END OF WRITE PAGE SETUP //

    // WRITING VELOCITY SETUP //
    cmd[0] = 0x5A;		// MODE SENSE
    cmd[2] = 0x2A;		// "Capabilities and Mechanical Status"
    cmd[8] = sizeof(header);	// header only to start with
    cmd[9] = 0;
    if (cmd.transport(READ,header,sizeof(header)))
	perror (":-( unable to MODE SENSE"), exit(1);

    len = (header[0]<<8|header[1])+2;
    if (len<30+8)	// no "Current Write Speed" present
	fprintf (stderr,":-[ Current Write Speed is not present\n"),
	exit(1);

    page2A = (unsigned char *)malloc(len);
    if (page2A == NULL)
	fprintf (stderr,":-( memory exhausted\n"), exit(1);
    p = page2A + 8 + (header[6]<<8|header[7]);

    int velocity0=0;
    do {
      int i,j,v,v0,minv,mini;
      unsigned char *vdp;

	cmd[0] = 0x5A;	// MODE SENSE
	cmd[2] = 0x2A;	// "Capabilities and Mechanical Status"
	cmd[7] = len>>8;
	cmd[8] = len;	// real length this time
	cmd[9] = 0;
	if (cmd.transport(READ,page2A,len))
	    fprintf (stderr,":-[ MODE SENSE failed with "
			    "SK=%xh/ASC=%02xh/ASCQ=%02xh\n",
			    sense[2]&0xF,sense[12],sense[13]),
	    exit(1);

	velocity = p[28]<<8|p[29];

	if (speed_factor)
	{   if (len<32+8)
		fprintf (stderr,":-[ Can't control writing velocity, "
				"skip -speed command line option.\n"),
		exit(1);

	    j=(p[30]<<8|p[31])*4;
	    vdp=p+32;
	    for (minv=0x7fffffff,i=0;i<j;i+=4)
	    {	v=vdp[i+2]<<8|vdp[i+3];
		if (v<minv) minv=v;
	    }
	    v0=speed_factor*minv;
	    for (mini=0,minv=0x7fffffff,i=0;i<j;i+=4)
	    {	v=abs(v0-(vdp[i+2]<<8|vdp[i+3]));
		if (v<minv) minv=v, mini=i;
	    } // mini is index of descriptor with closest velocity

	    velocity0=vdp[mini+2]<<8|vdp[mini+3];
	    speed_factor=0;

	    if (velocity0 != velocity)
	    {	cmd[0]=0xBB;		// SET CD SPEED
		cmd[1]=vdp[mini+1];	// Rotation Control
		cmd[2]=cmd[3]=0xff;	// Read Speed
		cmd[4]=vdp[mini+2];	// Write Speed
		cmd[5]=vdp[mini+3];
		cmd[11]=0;
		if (cmd.transport())
		    fprintf (stderr,":-[ SET CD SPEED failed with "
				    "SK=%xh/ASC=%02xh/ASCQ=%02xh\n",
				    sense[2]&0xF,sense[12],sense[13]),
		    exit(1);
	    }
	    else break;
	}
	else if (velocity0 && (velocity0 != velocity))
	    fprintf (stderr,":-[ Failed to change write speed: %d->%d\n",
			    velocity,velocity0),
	    exit(1);
	break;
    } while (1);

    velocity *= 1024;
    // END OF WRITING VELOCITY SETUP //

  return 0;
}

extern "C"
void minus_rw_finalize ()
{   while (media_written)
    { Scsi_Command cmd(media_fd);

	fprintf (stderr,"%s: flushing cache\n",media_device);
	cmd[0] = 0x35;		// FLUSH CACHE
	cmd[1] = 0x02;		// "IMMED"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to FLUSH CACHE");

	if (wait_for_unit (cmd,NULL)) break;

	fprintf (stderr,"%s: writing lead-out\n",media_device);
	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = 0x03;		// "Close Restricted Overwrite"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE SESSION");

	if (wait_for_unit (cmd,NULL)) break;

	media_written = 0;
    }
    _exit (errno);
}

extern "C"
void minus_r_finalize ()
{   while (media_written)
    { Scsi_Command cmd(media_fd);

	fprintf (stderr,"%s: flushing cache\n",media_device);
	cmd[0] = 0x35;		// FLUSH CACHE
	cmd[1] = 0x02;		// "IMMED"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to FLUSH CACHE");

	if (wait_for_unit (cmd,NULL)) break;

	fprintf (stderr,"%s: writing border-out\n",media_device);
	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = 0x01;		// "Close Track"
	cmd[4] = next_track>>8;
	cmd[5] = next_track;
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE TRACK");

	if (wait_for_unit (cmd,NULL)) break;

	fprintf (stderr,"%s: closing session\n",media_device);
	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 0x01;		// "IMMED"
	cmd[2] = 0x02;		// "Close Session"
	cmd[9] = 0;
	if (cmd.transport())
	    perror (":-( unable to CLOSE SESSION");

	if (wait_for_unit (cmd,NULL)) break;

	media_written = 0;
    }
    _exit (errno);
}
