/*
 * DVD±RW format 4.1 by Andy Polyakov <appro@fy.chalmers.se>.
 *
 * Use-it-on-your-own-risk, GPL bless...
 *
 * For further details see http://fy.chalmers.se/~appro/linux/DVD+RW/.
 *
 * Revision history:
 *
 * 2.0:
 * - deploy "IMMED" bit in "FORMAT UNIT";
 * 2.1:
 * - LP64 fix;
 * - USB workaround;
 * 3.0:
 * - C++-fication for better portability;
 * - SYSV signals for better portability;
 * - -lead-out option for improved DVD+RW compatibility;
 * - tested with SONY DRU-500A;
 * 4.0:
 * - support for DVD-RW formatting and blanking, tool name becomes
 *   overloaded...
 * 4.1:
 * - re-make it work under Linux 2.2 kernel;
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

#include "transport.hxx"

static void usage (char *prog)
{ fprintf (stderr,"- usage: %s [-force] [-lead-out|-blank[=full]] /dev/cdrom\n",prog),
  exit(1);
}

static volatile int *progress;

extern "C" void alarm_handler (int no)
{ static int         i=0,len=1,old_progress=0;
  static const char *str = "|/-\\",*backspaces="\b\b\b\b\b\b\b\b\b\b";
  int new_progress = *progress;

    if (new_progress != old_progress)
        len = fprintf (stderr,"%.*s%.1f%%",len,backspaces,
				100.0*new_progress/65536.0) - len,
	old_progress = new_progress;
    else
        fprintf (stderr,"\b%c",str[i]),
	i++, i&=3;

    alarm(1);
}

int main (int argc, char *argv[])
{ Scsi_Command			cmd;
  unsigned char			formats[260],dinfo[32],*sense=cmd.sense();
  char				*dev=NULL,*p;
  unsigned int			capacity,lead_out,mmc_profile;
  int				force=0,compat=0,blank=0,len,i;
  pid_t				pid;

    fprintf (stderr,"* DVD±RW format utility by <appro@fy.chalmers.se>, "
		    "version 4.1.\n");

    for (i=1;i<argc;i++) {
	if (*argv[i] == '-')
	    if (argv[i][1] == 'f')	force = 1;
	    else if (argv[i][1] == 'l')	force = compat = 1;
	    else if (argv[i][1] == 'b')
	    {	blank=0x11;
		if ((p=strchr(argv[i],'=')) && p[1]=='f') blank=0x10;
	    }
	    else			usage(argv[0]);
	else if (*argv[i] == '/')
	    dev = argv[i];
	else
	    usage (argv[0]);
    }

    if (dev==NULL) usage (argv[0]);

    if (!cmd.associate(dev))
	fprintf (stderr,"- [unable to open(\"%s\")]: ",dev), perror (NULL),
	exit(1);

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

    mmc_profile = formats[6]<<8|formats[7];

    if (mmc_profile!=0x1A && mmc_profile!=0x14 && mmc_profile!=0x13)
	fprintf (stderr, "- [mounted media doesn't appear to be DVD±RW]\n"),
	exit (1);

    /*
     * First figure out how long the actual list is. Problem here is
     * that (at least Linux) USB units absolutely insist on accurate
     * cgc.buflen and you can't just set buflen to arbitrary value
     * larger than actual transfer length.
     */
    cmd[0] = 0x23;		// READ FORMAT CAPACITIES
    cmd[8] = 4;
    cmd[9] = 0;
    if (cmd.transport(READ,formats,4))
	fprintf (stderr,"- [unable to READ FORMAT CAPACITIES (%xh/%xh/%xh)]: ",
			sense[2]&0xF,sense[12],sense[13]), perror (NULL),
	exit (1);

    len = formats[3];
    if (len&7 || len<16)
	fprintf (stderr,"- [allocation length isn't sane]\n"),
	exit(1);

    cmd[0] = 0x23;		// READ FORMAT CAPACITIES
    cmd[7] = (4+len)>>8;	// now with real length...
    cmd[8] = (4+len)&0xFF;
    cmd[9] = 0;
    if (cmd.transport(READ,formats,4+len))
	fprintf (stderr,"- [unable to READ FORMAT CAPACITIES (%xh/%xh/%xh)]: ",
			sense[2]&0xF,sense[12],sense[13]), perror (NULL),
	exit (1);

    if (len != formats[3])
	fprintf (stderr,"- [parameter length inconsistency]\n"),
	exit(1);

    if (mmc_profile == 0x1A)
    {	for (i=8;i<len;i+=8)	// look for "DVD+RW Full" descriptor
	    if ((formats [4+i+4]>>2) == 0x26) break;
    }
    else
    {	for (i=8;i<len;i+=8)	// look for "DVD-RW Quick" descriptor
	    if ((formats [4+i+4]>>2) == 0x15) break;
	if (i==len)
	{   fprintf (stderr,"- [failed to locate \"Quick Format\" descriptor]\n");
	    for (i=8;i<len;i+=8)// ... alternatively for "DVD-RW Full"
		if ((formats [4+i+4]>>2) == 0x10) break;
	}
    }

    if (i==len)
	fprintf (stderr,"- [can't locate appropriate format descriptor]\n"),
	exit(1);

    capacity = 0;
    capacity |= formats[4+i+0], capacity <<= 8;
    capacity |= formats[4+i+1], capacity <<= 8;
    capacity |= formats[4+i+2], capacity <<= 8;
    capacity |= formats[4+i+3];

    if (mmc_profile==0x1A)
	fprintf (stderr,"* %.1fGB DVD+RW media detected.\n",
			2048.0*capacity/1e9);
    else
	fprintf (stderr,"* %.1fGB DVD-RW media in %s mode detected.\n",
			2048.0*capacity/1e9,
			mmc_profile==0x13?"Restricted Overwrite":"Sequential");

    lead_out = 0;
    lead_out |= formats[4+0], lead_out <<= 8;
    lead_out |= formats[4+1], lead_out <<= 8;
    lead_out |= formats[4+2], lead_out <<= 8;
    lead_out |= formats[4+3];

    cmd[0] = 0x51;		// READ DISC INFORMATION
    cmd[8] = sizeof(dinfo);
    cmd[9] = 0;
    if (cmd.transport(READ,dinfo,sizeof(dinfo)))
	fprintf (stderr,"- [can't READ DISC INFORMATION]\n"),
	exit(1);

    if (dinfo[2]&3)		// non-blank media
    {	if (!force)
	{   if (mmc_profile == 0x1A || mmc_profile == 0x13)
		fprintf (stderr,"- media is already formatted, lead-out is currently at\n"
				"  %d KB which is %.1f%% of total capacity.\n",
				lead_out*2,(100.0*lead_out)/capacity);
	    else
		fprintf (stderr,"- media is not blank\n");
	    if (mmc_profile == 0x1A)
		fprintf (stderr,"- you have the option to rerun with:\n"
				"   -lead-out  to elicit lead-out relocation for better\n"
				"              DVD-ROM compatibility, data is not affected;\n"
				"   -force     to enforce new format (not recommended)\n"
				"              and wipe the data.\n");
	    else
		fprintf (stderr,"- you have the option to rerun with:\n"
				"   -force        to enforce new format or mode transition\n"
				"                 and wipe the data;\n"
				"   -blank[=full] to change to Sequential mode.\n");    
	    exit (0);
	}
	else if (cmd.umount())
	    perror ("- [unable to umount]"),
	    exit (1);
    }
    else
	force = 0;

    { int fd;
      char *s;

	if ((fd=mkstemp (s=strdup("/tmp/dvd+rw.XXXXXX"))) < 0)
	    fprintf (stderr,"- [unable to mkstemp(\"%s\")]",s),
	    exit(1);

	ftruncate(fd,sizeof(*progress));
	unlink(s);

	progress = (int *)mmap(NULL,sizeof(*progress),PROT_READ|PROT_WRITE,
				MAP_SHARED,fd,0);
	close (fd);
	if (progress == MAP_FAILED)
            perror ("- [unable to mmap anonymously]"),
	    exit(1);
    }
    *progress = 0;

    if ((pid=fork()) == (pid_t)-1)
	perror ("- [unable to fork()]"),
	exit(1);

    if (pid)
    { struct sigaction sa;

	cmd.~Scsi_Command();
	if (compat && force)
	    fprintf (stderr,"* relocating lead-out .");
	else if (blank)
	    fprintf (stderr,"* blanking .");
	else
	    fprintf (stderr,"* formatting .");
	sigaction (SIGALRM,NULL,&sa);
	sa.sa_flags &= ~SA_RESETHAND;
	sa.sa_flags |= SA_RESTART;
	sa.sa_handler = alarm_handler;
	sigaction (SIGALRM,&sa,NULL);
	alarm(1);
	while ((waitpid(pid,&i,0) != pid) && !WIFEXITED(i)) ;
	if (WEXITSTATUS(i) == 0) fprintf (stderr,"\n");
	exit (0);
    }

    /*
     * You can suspend, terminate, etc. the parent. We will keep on
     * working in background...
     */
    setsid();
    signal(SIGHUP,SIG_IGN);
    signal(SIGINT,SIG_IGN);
    signal(SIGTERM,SIG_IGN);

    // formats[i] becomes "Format Unit Parameter List"
    formats[i+0] = 0;		// "Reserved"
    formats[i+1] = 2;		// "IMMED" flag
    formats[i+2] = 0;		// "Descriptor Length" (MSB)
    formats[i+3] = 8;		// "Descriptor Length" (LSB)

    if (mmc_profile == 0x1A)
    {	if (compat && force)
	    formats[i+4+0]=formats[i+4+1]=formats[i+4+2]=formats[i+4+3]=0,
	    formats[i+4+7] = 1;	// "Restart format"

	cmd[0] = 0x04;		// FORMAT UNIT
	cmd[1] = 0x11;		// "FmtData" and "Format Code"
	cmd[5] = 0;
	if (cmd.transport(WRITE,formats+i,12))
	    fprintf (stderr,"- [unable to FORMAT UNIT (%xh/%xh/%xh)]: ",
			    sense[2]&0xF,sense[12],sense[13]),
	    perror (NULL),
	    exit(1);

	if (wait_for_unit (cmd,progress)) exit (1);

	if (!compat)
	{   cmd[0] = 0x5B;	// CLOSE TRACK/SESSION
	    cmd[1] = 1;		// "IMMED" flag on
	    cmd[2] = 0;		// "Stop De-Icing"
	    cmd[9] = 0;
	    if (cmd.transport())
		fprintf (stderr,"- [unable to CLOSE SESSION (%xh/%xh/%xh)]: ",
				sense[2]&0xF,sense[12],sense[13]),
		perror (NULL),
		exit(1);

	    if (wait_for_unit (cmd,progress)) exit (1);
	}

	cmd[0] = 0x5B;		// CLOSE TRACK/SESSION
	cmd[1] = 1;		// "IMMED" flag on
	cmd[2] = 2;		// "Close Session"
	cmd[9] = 0;
	if (cmd.transport())
	    fprintf (stderr,"- [unable to CLOSE SESSION (%xh/%xh/%xh)]: ",
			    sense[2]&0xF,sense[12],sense[13]),
	    perror (NULL),
	    exit(1);

	if (wait_for_unit (cmd,progress)) exit (1);
    }
    else
    {	if (blank)
    	{   cmd[0] = 0xA1;	// BLANK
	    cmd[1] = blank;
	    cmd[11] = 0;
	    if (cmd.transport())
		fprintf (stderr,"- [unable to BLANK (%xh/%xh/%xh)]: ",
				sense[2]&0xF,sense[12],sense[13]),
		perror (NULL),
		exit(1);

	    if (wait_for_unit (cmd,progress)) exit (1);
	}
	else
	{   cmd[0] = 0x04;	// FORMAT UNIT
	    cmd[1] = 0x11;	// "FmtData" and "Format Code"
	    cmd[5] = 0;
	    if (cmd.transport(WRITE,formats+i,12))
		fprintf (stderr,"- [unable to FORMAT UNIT (%xh/%xh/%xh)]: ",
				sense[2]&0xF,sense[12],sense[13]),
		perror (NULL),
		exit(1);

	    if (wait_for_unit (cmd,progress)) exit (1);
	}
    }

    if (cmd.reload())
	perror ("- [unable to reload tray]"), exit(1);

  return 0;
}
