/*
 * DVD+RW format 2.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;
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/cdrom.h>
#include <asm/param.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

void usage (char *prog)
{ fprintf (stderr,"- usage: %s [-f] /dev/cdrom\n",prog),
  exit(1);
}

volatile int *progress;

void alarm_handler (int no)
{ static int	i=0,len=1,old_progress=0;
  static 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);
}

void wait_for_unit (int fd)
{ struct cdrom_generic_command	cgc;
  struct request_sense sense;

    while (1)
    {   sleep(1);
        memset (&cgc,0,sizeof(cgc));
	cgc.cmd[0] = 0;		/* TEST UNIT READY	*/
	cgc.quiet = 1;
	cgc.data_direction = CGC_DATA_NONE;
	memset (&sense,0,sizeof(sense));
	cgc.sense = &sense;
	if (ioctl (fd,CDROM_SEND_PACKET,&cgc) == 0) break;
	/*
	 * I wish I could test for sense.valid, but (at least) hp dvd100i
	 * returns 0 in valid bit at this point:-(
	 */
	if (sense.error_code == 0)
	    perror ("- [unable to TEST UNIT READY]"), exit(1);
	/*
	 * MMC-3 (draft) specification says that the unit should return
	 * progress indicator in key specific bytes even in reply to TEST
	 * UNIT READY. But (at least) hp dvd100i doesn't and I have to
	 * fetch it separately:-(
	 */
        memset (&cgc,0,sizeof(cgc));
	cgc.buffer = (void *)&sense;
	cgc.buflen = 18; /*sizeof (sense);*/
	cgc.data_direction = CGC_DATA_READ;
	cgc.cmd[0] = 0x03;	/* REQUEST SENSE	*/
	cgc.cmd[4] = 18; /*sizeof (sense);*/
	if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	    perror ("- [unable to REQUEST SENSE]"), exit(1);
	if (sense.sks[0]&0x80)
	    *progress = sense.sks[1]<<8|sense.sks[2];
    }
}

int main (int argc, char *argv[])
{ int				fd;
  struct cdrom_generic_command	cgc;
  struct request_sense		sense;
  struct stat			sb;
  unsigned char			formats[260],*dev=NULL;
  unsigned int			capacity,lead_out;
  int				force=0,len,i;
  pid_t				pid;

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

    for (i=1;i<argc;i++) {
	if (*argv[i] == '-')
	    if (argv[i][1] == 'f') force = 1;
	    else usage(argv[0]);
	else if (*argv[i] == '/')
	    dev = argv[i];
	else
	    usage (argv[0]);
    }

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

    if ((fd = open(dev,O_RDONLY|O_NONBLOCK)) < 0)
	fprintf (stderr,"- [unable to open(\"%s\")]: ",dev), perror (NULL),
	exit(1);

    if (fstat(fd,&sb) < 0)
	fprintf (stderr,"- [unable to stat(\"%s\")]: ",dev), perror (NULL),
	exit(1);

    if (!S_ISBLK(sb.st_mode))
	fprintf (stderr,"- [%s is not a block device]\n",dev),
	exit(1);

    memset (&cgc,0,sizeof(cgc));
    cgc.buffer = formats;
    cgc.buflen = 8;
    cgc.data_direction = CGC_DATA_READ;
    cgc.cmd[0] = 0x46;	/* GET CONFIGURATION	*/
    cgc.cmd[8] = 8;
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	perror ("- [unable to GET CONFIGURATION]"),
	exit (1);

    if ((formats[6]<<8|formats[7]) != 0x1A)
	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) USB units absolutely insist on accurate
     * cgc.buflen and you can't just set buflen to arbitrary value
     * larger than actual transfer length.
     */
    memset (&cgc,0,sizeof(cgc));
    cgc.buffer = formats;
    cgc.buflen = 4;
    cgc.data_direction = CGC_DATA_READ;
    cgc.sense  = &sense;
    cgc.cmd[0] = 0x23;	/* READ FORMAT CAPACITIES	*/
    cgc.cmd[8] = 4;
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	fprintf (stderr,"- [unable to READ FORMAT CAPACITIES (%xh/%xh/%xh)]: ",
			sense.sense_key,sense.asc,sense.ascq), perror (NULL),
	exit (1);

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

    memset (&cgc,0,sizeof(cgc));
    cgc.buffer = formats;
    cgc.buflen = 4+len;
    cgc.data_direction = CGC_DATA_READ;
    cgc.sense  = &sense;
    cgc.cmd[0] = 0x23;	/* READ FORMAT CAPACITIES	*/
    cgc.cmd[7] = (4+len)>>8;
    cgc.cmd[8] = (4+len)&0xFF;
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	fprintf (stderr,"- [unable to READ FORMAT CAPACITIES (%xh/%xh/%xh)]: ",
			sense.sense_key,sense.asc,sense.ascq), perror (NULL),
	exit (1);

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

    for (i=8;i<len;i+=8)
	if ((formats [4+i+4]>>2) == 0x26) 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];

    fprintf (stderr,"* %.1fGB DVD+RW media detected.\n",2048.0*capacity/1e9);

    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];

    if (formats[8] == 2 && !force)
	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),
	fprintf (stderr,"- you have the option to rerun with -f flag to enforce\n"
			"  new format (not recommended).\n"),
	exit (0);

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

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

    if (pid)
    {	close (fd);
        fprintf (stderr,"* formatting .");
	signal (SIGALRM,alarm_handler);
	alarm(1);
	while ((waitpid(pid,&i,0) != pid) && !WIFEXITED(i)) ;
	if (WEXITSTATUS(i) == 0) fprintf (stderr,"\n");
	return 0;
    }

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

    memset (&cgc,0,sizeof(cgc));
    /* formats[i] becomes "Format Unit Parameter List"	*/
    cgc.buffer = formats+i;
    cgc.buflen = 12;
    cgc.data_direction = CGC_DATA_WRITE;
    cgc.sense = &sense;
    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)	*/
    cgc.cmd[0] = 0x04;  /* FORMAT UNIT			*/
    cgc.cmd[1] = 0x11;	/* "FmtData" and "Format Code"	*/
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	fprintf (stderr,"- [unable to FORMAT UNIT (%xh/%xh/%xh)]: ",
			sense.sense_key,sense.asc,sense.ascq), perror (NULL),
	exit(1);

    wait_for_unit (fd);

#if 0
    memset (&cgc,0,sizeof(cgc));
    cgc.data_direction = CGC_DATA_NONE;
    cgc.cmd[0] = 0x35;  /* FLUSH CACHE		*/
    ioctl (fd,CDROM_SEND_PACKET,&cgc);
#endif

    memset (&cgc,0,sizeof(cgc));
    cgc.data_direction = CGC_DATA_NONE;
    cgc.cmd[0] = 0x5B;	/* CLOSE TRACK/SESSION	*/
    cgc.cmd[1] = 1;	/* "IMMED" flag	set	*/
    cgc.cmd[2] = 0;	/* "Stop De-Icing"	*/
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	fprintf (stderr,"- [unable to CLOSE SESSION (%xh/%xh/%xh)]: ",
			sense.sense_key,sense.asc,sense.ascq), perror (NULL),
	exit(1);

    wait_for_unit (fd);

    memset (&cgc,0,sizeof(cgc));
    cgc.data_direction = CGC_DATA_NONE;
    cgc.cmd[0] = 0x5B;	/* CLOSE TRACK/SESSION	*/
    cgc.cmd[1] = 1;	/* "IMMED" flag	set	*/
    cgc.cmd[2] = 0x02;	/* "Close session"	*/
    if (ioctl (fd,CDROM_SEND_PACKET,&cgc))
	fprintf (stderr,"- [unable to CLOSE SESSION (%xh/%xh/%xh)]: ",
			sense.sense_key,sense.asc,sense.ascq), perror (NULL),
	exit(1);

    wait_for_unit (fd);


    /* see if kernel was patched to support DVD+RW */
#if 0
    if (ioctl (fd,CDROM_MEDIA_CHANGED,0) == 0)
#else
    if (open (dev,O_RDWR|O_NONBLOCK) < 0 && errno == EROFS)
    /* yes, I leaked a file descriptor, but I'm on my way out */
#endif
    {	if (ioctl (fd,CDROMEJECT))
	    perror ("- [failed to OPEN TRAY]"), exit (1);
	if (ioctl (fd,CDROMCLOSETRAY))
	    perror ("- [failed to CLOSE TRAY]"), exit (1);
    }

  return 0;
}
