/*
 * Copyright (C) 2004, 2005 Rocky Bernstein <rocky@panix.com>
 *           (C) 1998 Monty <xiphmont@mit.edu>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2, or (at your option)
 *   any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 * See ChangeLog for recent changes.
 *
 * last changes:
 *   22.01.98 - first version
 *   15.02.98 - alpha 2: juggled two includes from interface/low_interface.h
 *                       that move contents in Linux 2.1
 *                       Linked status bar to isatty to avoid it appearing
 *                       in a redirected file.
 *                       Played with making TOC less verbose.
 *   04.04.98 - alpha 3: zillions of bugfixes, also added MMC and IDE_SCSI
 *                       emulation support
 *   05.04.98 - alpha 4: Segfault fix, cosmetic repairs
 *   05.04.98 - alpha 5: another segfault fix, cosmetic repairs, 
 *                       Gadi Oxman provided code to identify/fix nonstandard
 *                       ATAPI CDROMs 
 *   07.04.98 - alpha 6: Bugfixes to autodetection
 *   18.06.98 - alpha 7: Additional SCSI error handling code
 *                       cosmetic fixes
 *                       segfault fixes
 *                       new sync/silence code, smaller fft      
 *   15.07.98 - alpha 8: More new SCSI code, better error recovery
 *                       more segfault fixes (two linux bugs, one my fault)
 *                       Fixup reporting fixes, smilie fixes.
 *                       AIFF support (in addition to AIFC)
 *                       Path parsing fixes
 *   Changes are becoming TNTC. Will resume the log at beta.
 *
 *   09.04.04 - conversion to use libcdio. See top-level Changelog for
 *              libcdio history.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef HAVE_STDIO_H
# include <stdio.h>
#endif

#ifdef HAVE_STDARG_H
# include <stdarg.h>
#endif

#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif 

#ifdef HAVE_STRING_H
# include <string.h>
#endif

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include <getopt.h>

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif

#include <math.h>
#include <sys/time.h>

#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif

#include <cdio/cdio.h>
#include <cdio/cd_types.h>
#include <cdio/cdda.h>
#include <cdio/paranoia.h>
#include <cdio/bytesex.h>
#include "utils.h"
#include "report.h"
#include "version.h"
#include "header.h"
#include "buffering_write.h"

extern int verbose;
extern int quiet;

/* I wonder how many alignment issues this is gonna trip in the
   future...  it shouldn't trip any...  I guess we'll find out :) */

static int 
bigendianp(void)
{
  int test=1;
  char *hack=(char *)(&test);
  if(hack[0])return(0);
  return(1);
}

static long 
parse_offset(cdrom_drive_t *d, char *offset, int begin)
{
  track_t i_track= CDIO_INVALID_TRACK;
  long hours   = -1;
  long minutes = -1;
  long seconds = -1;
  long sectors = -1;
  char *time   = NULL;
  char *temp   = NULL;
  long ret;

  if (!offset) return -1;

  /* seperate track from time offset */
  temp=strchr(offset,']');
  if(temp){
    *temp='\0';
    temp=strchr(offset,'[');
    if(temp==NULL){
      report("Error parsing span argument");
      exit(1);
    }
    *temp='\0';
    time=temp+1;
  }

  /* parse track */
  {
    int chars=strspn(offset,"0123456789");
    if(chars>0){
      offset[chars]='\0';
      i_track=atoi(offset);
      if ( i_track > d->tracks ) {
	/*take track i_first_track-1 as pre-gap of 1st track*/
	char buffer[256];
	snprintf(buffer, sizeof(buffer), 
		 "Track #%d does not exist.", i_track);
	report(buffer);
	exit(1);
      }
    }
  }

  while(time){
    long val,chars;
    char *sec=strrchr(time,'.');
    if(!sec)sec=strrchr(time,':');
    if(!sec)sec=time-1;

    chars=strspn(sec+1,"0123456789");
    if(chars)
      val=atoi(sec+1);
    else
      val=0;
    
    switch(*sec){
    case '.':
      if(sectors!=-1){
	report("Error parsing span argument");
	exit(1);
      }
      sectors=val;
      break;
    default:
      if(seconds==-1)
	seconds=val;
      else
	if(minutes==-1)
	  minutes=val;
	else
	  if(hours==-1)
	    hours=val;
	  else{
	    report("Error parsing span argument");
	    exit(1);
	  }
      break;
    }
	 
    if (sec<=time) break;
    *sec='\0';
  }

  if (i_track == CDIO_INVALID_TRACK) {
    if (seconds==-1 && sectors==-1) return -1;
    if (begin==-1) {
      ret=cdda_disc_firstsector(d);
    } else
      ret = begin;
  } else {
    if ( seconds==-1 && sectors==-1 ) {
      if (begin==-1){ 
	/* first half of a span */
	return(cdda_track_firstsector(d, i_track));
      }else{
	return(cdda_track_lastsector(d, i_track));
      }
    } else {
      /* relative offset into a track */
      ret=cdda_track_firstsector(d, i_track);
    }
  }
   
  /* OK, we had some sort of offset into a track */

  if (sectors != -1) ret += sectors;
  if (seconds != -1) ret += seconds*CDIO_CD_FRAMES_PER_SEC;
  if (minutes != -1) ret += minutes*CDIO_CD_FRAMES_PER_MIN;
  if (hours   != -1) ret += hours  *60*CDIO_CD_FRAMES_PER_MIN;

  /* We don't want to outside of the track; if it's relative, that's OK... */
  if( i_track != CDIO_INVALID_TRACK ){
    if (cdda_sector_gettrack(d,ret) != i_track) {
      report("Time/sector offset goes beyond end of specified track.");
      exit(1);
    }
  }

  /* Don't pass up end of session */

  if( ret>cdda_disc_lastsector(d) ) {
    report("Time/sector offset goes beyond end of disc.");
    exit(1);
  }

  return(ret);
}

static void 
display_toc(cdrom_drive_t *d)
{
  long audiolen=0;
  track_t i;
  
  report("\nTable of contents (audio tracks only):\n"
	 "track        length               begin        copy pre ch\n"
	 "===========================================================");
  
  for( i=1; i<=d->tracks; i++)
    if ( cdda_track_audiop(d,i) ) {
      char buffer[256];

      lsn_t sec=cdda_track_firstsector(d,i);
      lsn_t off=cdda_track_lastsector(d,i)-sec+1;

      sprintf(buffer,
	      "%3d.  %7ld [%02d:%02d.%02d]  %7ld [%02d:%02d.%02d]  %s %s %s",
	      i,
	      (long int) off,
	      (int) (off/(CDIO_CD_FRAMES_PER_MIN)),
	      (int) ((off/CDIO_CD_FRAMES_PER_SEC) % CDIO_CD_SECS_PER_MIN),
	      (int) (off % CDIO_CD_FRAMES_PER_SEC),
	      (long int) sec,
	      (int) (sec/(CDIO_CD_FRAMES_PER_MIN)),
	      (int) ((sec/CDIO_CD_FRAMES_PER_SEC) % CDIO_CD_SECS_PER_MIN),
	      (int) (sec % CDIO_CD_FRAMES_PER_SEC),
	      cdda_track_copyp(d,i)?"  OK":"  no",
	      cdda_track_preemp(d,i)?" yes":"  no",
	      cdda_track_channels(d,i)==2?" 2":" 4");
      report(buffer);
      audiolen+=off;
    }
  {
    char buffer[256];
    sprintf(buffer, "TOTAL %7ld [%02d:%02d.%02d]    (audio only)",
	    audiolen,
	    (int) (audiolen/(CDIO_CD_FRAMES_PER_MIN)),
	    (int) ((audiolen/CDIO_CD_FRAMES_PER_SEC) % CDIO_CD_SECS_PER_MIN),
	    (int) (audiolen % CDIO_CD_FRAMES_PER_SEC));
      report(buffer);
  }
  report("");
}

#include "usage.h"
static void usage(FILE *f)
{
  fprintf( f, usage_help);
}

long callbegin;
long callend;
long callscript=0;

static const char *callback_strings[15]={
  "wrote",
  "finished",
  "read",
  "verify",
  "jitter",
  "correction",
  "scratch",
  "scratch repair",
  "skip",
  "drift",
  "backoff",
  "overlap",
  "dropped",
  "duped",
  "transport error"};

static int skipped_flag=0;
static int abort_on_skip=0;
static void 
callback(long int inpos, paranoia_cb_mode_t function)
{
  /*

 (== PROGRESS == [--+!---x-------------->           | 007218 01 ] == :-) . ==) 

 */

  int graph=30;
  char buffer[256];
  static long c_sector=0,v_sector=0;
  static char dispcache[30]="                              ";
  static int last=0;
  static long lasttime=0;
  long sector,osector=0;
  struct timeval thistime;
  static char heartbeat=' ';
  int position=0,aheadposition=0;
  static int overlap=0;
  static int printit=-1;

  static int slevel=0;
  static int slast=0;
  static int stimeout=0;
  const char *smilie="= :-)";
  
  if(callscript)
    fprintf(stderr,"##: %d [%s] @ %ld\n",
	    function,(function>=-2&&function<=13?callback_strings[function+2]:
		      ""),inpos);

  if(!quiet){
    long test;
    osector=inpos;
    sector=inpos/CD_FRAMEWORDS;
    
    if(printit==-1){
      if(isatty(STDERR_FILENO)){
	printit=1;
      }else{
	printit=0;
      }
    }

    if(printit==1){  /* else don't bother; it's probably being 
			redirected */
      position=((float)(sector-callbegin)/
		(callend-callbegin))*graph;
      
      aheadposition=((float)(c_sector-callbegin)/
		     (callend-callbegin))*graph;
      
      if(function==-2){
	v_sector=sector;
	return;
      }
      if(function==-1){
	last=8;
	heartbeat='*';
	slevel=0;
	v_sector=sector;
      }else
	if(position<graph && position>=0)
	  switch(function){
	  case PARANOIA_CB_VERIFY:
	    if(stimeout>=30){
	      if(overlap>CD_FRAMEWORDS)
		slevel=2;
	      else
		slevel=1;
	    }
	    break;
	  case PARANOIA_CB_READ:
	    if(sector>c_sector)c_sector=sector;
	    break;
	    
	  case PARANOIA_CB_FIXUP_EDGE:
	    if(stimeout>=5){
	      if(overlap>CD_FRAMEWORDS)
		slevel=2;
	      else
		slevel=1;
	    }
	    if(dispcache[position]==' ') 
	      dispcache[position]='-';
	    break;
	  case PARANOIA_CB_FIXUP_ATOM:
	    if(slevel<3 || stimeout>5)slevel=3;
	    if(dispcache[position]==' ' ||
	       dispcache[position]=='-')
	      dispcache[position]='+';
	    break;
	  case PARANOIA_CB_READERR:
	    slevel=6;
	    if(dispcache[position]!='V')
	      dispcache[position]='e';
	    break;
	  case PARANOIA_CB_SKIP:
	    slevel=8;
	    dispcache[position]='V';
	    break;
	  case PARANOIA_CB_OVERLAP:
	    overlap=osector;
	    break;
	  case PARANOIA_CB_SCRATCH:
	    slevel=7;
	    break;
	  case PARANOIA_CB_DRIFT:
	    if(slevel<4 || stimeout>5)slevel=4;
	    break;
	  case PARANOIA_CB_FIXUP_DROPPED:
	  case PARANOIA_CB_FIXUP_DUPED:
	    slevel=5;
	    if(dispcache[position]==' ' ||
	       dispcache[position]=='-' ||
	       dispcache[position]=='+')
	      dispcache[position]='!';
	    break;
          case PARANOIA_CB_REPAIR:
          case PARANOIA_CB_BACKOFF:
	    break;
	  }
    
      switch(slevel){
      case 0:  /* finished, or no jitter */
	if(skipped_flag)
	  smilie=" 8-X";
	else
	  smilie=" :^D";
	break;
      case 1:  /* normal.  no atom, low jitter */
	smilie=" :-)";
	break;
      case 2:  /* normal, overlap > 1 */
	smilie=" :-|";
	break; 
      case 4:  /* drift */
	smilie=" :-/";
	break;
      case 3:  /* unreported loss of streaming */
	smilie=" :-P";
	break;
      case 5:  /* dropped/duped bytes */
	smilie=" 8-|";
	break;
      case 6:  /* scsi error */
	smilie=" :-0";
	break;
      case 7:  /* scratch */
	smilie=" :-(";
	break;
      case 8:  /* skip */
	smilie=" ;-(";
	skipped_flag=1;
	break;
	
      }
      
      gettimeofday(&thistime,NULL);
      test=thistime.tv_sec*10+thistime.tv_usec/100000;

      if(lasttime!=test || function==-1 || slast!=slevel){
	if(lasttime!=test || function==-1){
	  last++;
	  lasttime=test;
	  if(last>7)last=0;
	  stimeout++;
	  switch(last){
	  case 0:
	    heartbeat=' ';
	    break;
	  case 1:case 7:
	    heartbeat='.';
	    break;
	  case 2:case 6:
	    heartbeat='o';
	    break;
	  case 3:case 5:  
	    heartbeat='0';
	    break;
	  case 4:
	    heartbeat='O';
	    break;
	  }
	  if(function==-1)
	    heartbeat='*';

	}
	if(slast!=slevel){
	  stimeout=0;
	}
	slast=slevel;
	
	if(abort_on_skip && skipped_flag && function !=-1){
	  sprintf(buffer,
		  "\r (== PROGRESS == [%s| %06ld %02d ] ==%s %c ==)   ",
		  "  ...aborting; please wait... ",
		  v_sector,overlap/CD_FRAMEWORDS,smilie,heartbeat);
	}else{
	  if(v_sector==0)
	    sprintf(buffer,
		    "\r (== PROGRESS == [%s| ...... %02d ] ==%s %c ==)   ",
		    dispcache,overlap/CD_FRAMEWORDS,smilie,heartbeat);
	  
	  else
	    sprintf(buffer,
		    "\r (== PROGRESS == [%s| %06ld %02d ] ==%s %c ==)   ",
		    dispcache,v_sector,overlap/CD_FRAMEWORDS,smilie,heartbeat);
	  
	  if(aheadposition>=0 && aheadposition<graph && !(function==-1))
	    buffer[aheadposition+19]='>';
	}
   
	fprintf(stderr,buffer);
      }
    }
  }

  /* clear the indicator for next batch */
  if(function==-1)
    memset(dispcache,' ',graph);
}

const char *optstring = "escCn:o:O:d:g:S:prRwafvqVQhZz::YXWBi:Tt:x:";

struct option options [] = {
	{"stderr-progress",           no_argument,       NULL, 'e'},
	{"search-for-drive",          no_argument,       NULL, 's'},
	{"force-cdrom-little-endian", no_argument,       NULL, 'c'},
	{"force-cdrom-big-endian",    no_argument,       NULL, 'C'},
	{"force-default-sectors",     required_argument, NULL, 'n'},
	{"force-search-overlap",      required_argument, NULL, 'o'},
	{"force-cdrom-device",        required_argument, NULL, 'd'},
	{"force-generic-device",      required_argument, NULL, 'g'},
	{"force-read-speed",          required_argument, NULL, 'S'},
	{"sample-offset",             required_argument, NULL, 'O'},
	{"toc-offset",                required_argument, NULL, 't'},
	{"toc-bias",                  no_argument,       NULL, 'T'},
	{"output-raw",                no_argument,       NULL, 'p'},
	{"output-raw-little-endian",  no_argument,       NULL, 'r'},
	{"output-raw-big-endian",     no_argument,       NULL, 'R'},
	{"output-wav",                no_argument,       NULL, 'w'},
	{"output-aiff",               no_argument,       NULL, 'f'},
	{"output-aifc",               no_argument,       NULL, 'a'},
	{"batch",                     no_argument,       NULL, 'B'},
	{"verbose",                   no_argument,       NULL, 'v'},
	{"quiet",                     no_argument,       NULL, 'q'},
	{"version",                   no_argument,       NULL, 'V'},
	{"query",                     no_argument,       NULL, 'Q'},
	{"help",                      no_argument,       NULL, 'h'},
	{"disable-paranoia",          no_argument,       NULL, 'Z'},
	{"disable-extra-paranoia",    no_argument,       NULL, 'Y'},
	{"abort-on-skip",             no_argument,       NULL, 'X'},
	{"test-mode",                 required_argument, NULL, 'x'},
	{"disable-fragmentation",     no_argument,       NULL, 'F'},
	{"output-info",               required_argument, NULL, 'i'},
	{"never-skip",                optional_argument, NULL, 'z'},

	{NULL,0,NULL,0}
};

static cdrom_drive_t    *d        = NULL;
static cdrom_paranoia_t *p        = NULL;
static char             *span     = NULL;
static char *force_cdrom_device   = NULL;
static char *info_file            = NULL;

#define free_and_null(p) \
  if (p) free(p);	 \
  p=NULL;		 
  
static void 
cleanup (void) 
{
  if (p) paranoia_free(p);
  if (d) cdda_close(d);
  free_and_null(force_cdrom_device);
  free_and_null(span);
  free_and_null(info_file);
}

int 
main(int argc,char *argv[])
{
  int   toc_bias             =  0;
  int   toc_offset           =  0;
  int   sample_offset        =  0;
  int   force_cdrom_endian   = -1;
  int   force_cdrom_sectors  = -1;
  int   force_cdrom_overlap  = -1;
  int   force_cdrom_speed    = -1;
  int   test_flags           =  0;
  int   max_retries          = 20;
  int   output_type          =  1; /* 0=raw, 1=wav, 2=aifc */
  int   output_endian        =  0; /* -1=host, 0=little, 1=big */
  int   query_only           =  0;
  int   batch                =  0;

  /* full paranoia, but allow skipping */
  int paranoia_mode=PARANOIA_MODE_FULL^PARANOIA_MODE_NEVERSKIP; 

  int out;

  int search=0;
  int c,long_option_index;

  atexit(cleanup);

  while((c=getopt_long(argc,argv,optstring,options,&long_option_index))!=EOF){
    switch(c){
    case 'B':
      batch=1;
      break;
    case 'c':
      force_cdrom_endian=0;
      break;
    case 'C':
      force_cdrom_endian=1;
      break;
    case 'n':
      force_cdrom_sectors=atoi(optarg);
      break;
    case 'o':
      force_cdrom_overlap=atoi(optarg);
      break;
    case 'g':
    case 'd':
      if (force_cdrom_device) {
	fprintf(stderr,
		"Multiple cdrom devices given. Previous device %s ignored\n",
		force_cdrom_device);
	free(force_cdrom_device);
      }
      force_cdrom_device=strdup(optarg);
      break;
    case 'S':
      force_cdrom_speed=atoi(optarg);
      break;
    case 'p':
      output_type=0;
      output_endian=-1;
      break;
    case 'r':
      output_type=0;
      output_endian=0;
      break;
    case 'R':
      output_type=0;
      output_endian=1;
      break;
    case 'w':
      output_type=1;
      output_endian=0;
      break;
    case 'a':
      output_type=2;
      output_endian=1;
      break;
    case 'f':
      output_type=3;
      output_endian=1;
      break;
    case 'v':
      verbose=CDDA_MESSAGE_PRINTIT;
      quiet=0;
      break;
    case 's':
      search=1;
      break;
    case 'q':
      verbose=CDDA_MESSAGE_FORGETIT;
      quiet=1;
      break;
    case 'e':
      callscript=1;
      fprintf(stderr,
	      "Sending all callback output to stderr for wrapper script\n");
      break;
    case 'V':
      fprintf(stderr,PARANOIA_VERSION);
      fprintf(stderr,"\n");
      exit(0);
      break;
    case 'Q':
      query_only=1;
      break;
    case 'h':
      usage(stdout);
      exit(0);
    case 'Z':
      paranoia_mode=PARANOIA_MODE_DISABLE; 
      break;
    case 'z':
      if (optarg) {
        max_retries = atoi (optarg);
        paranoia_mode&=~PARANOIA_MODE_NEVERSKIP;
      } else {
        paranoia_mode|=PARANOIA_MODE_NEVERSKIP;
      }
      break;
    case 'Y':
      paranoia_mode|=PARANOIA_MODE_OVERLAP; /* cdda2wav style overlap 
						check only */
      paranoia_mode&=~PARANOIA_MODE_VERIFY;
      break;
    case 'X':
      /*paranoia_mode&=~(PARANOIA_MODE_SCRATCH|PARANOIA_MODE_REPAIR);*/
      abort_on_skip=1;
      break;
    case 'W':
      paranoia_mode&=~PARANOIA_MODE_REPAIR;
      break;
    case 'F':
      paranoia_mode&=~(PARANOIA_MODE_FRAGMENT);
      break;
    case 'i':
      if(info_file)free(info_file);
      info_file=strdup(info_file);
      break;
    case 'T':
      toc_bias=-1;
      break;
    case 't':
      toc_offset=atoi(optarg);
      break;
    case 'O':
      sample_offset=atoi(optarg);
      break;
    case 'x':
      test_flags=atoi(optarg);
      break;
    default:
      usage(stderr);
      exit(1);
    }
  }

  if(optind>=argc && !query_only){
    if(batch)
      span=NULL;
    else{
      /* D'oh.  No span. Fetch me a brain, Igor. */
      usage(stderr);
      exit(1);
    }
  }else
    if (argv[optind]) span=strdup(argv[optind]);

  report(PARANOIA_VERSION);

  /* Query the cdrom/disc; we may need to override some settings */

  if(force_cdrom_device)
    d=cdda_identify(force_cdrom_device,verbose,NULL);
  else {
    driver_id_t driver_id;
    char **ppsz_cd_drives = cdio_get_devices_with_cap_ret(NULL,  
							  CDIO_FS_AUDIO, 
							  false,
							  &driver_id);
    if (ppsz_cd_drives && *ppsz_cd_drives) {
      d=cdda_identify(*ppsz_cd_drives,verbose, NULL);
    } else {
      report("\nUnable find or access a CD-ROM drive with an audio CD"
	     " in it.");
      report("\nYou might try specifying the drive, especially if it has"
	     " mixed-mode (and non-audio) format tracks");
      exit(1);
    }
    
    cdio_free_device_list(ppsz_cd_drives);
    free(ppsz_cd_drives);
  }
  
  if(!d){
    if(!verbose)
      report("\nUnable to open cdrom drive; -v might give more information.");
    exit(1);
  }
  
  if(verbose)
    cdda_verbose_set(d,CDDA_MESSAGE_PRINTIT,CDDA_MESSAGE_PRINTIT);
  else
    cdda_verbose_set(d,CDDA_MESSAGE_PRINTIT,CDDA_MESSAGE_FORGETIT);

  /* possibly force hand on endianness of drive, sector request size */
  if(force_cdrom_endian!=-1){
    d->bigendianp=force_cdrom_endian;
    switch(force_cdrom_endian){
    case 0:
      report("Forcing CDROM sense to little-endian; ignoring preset and autosense");
      break;
    case 1:
      report("Forcing CDROM sense to big-endian; ignoring preset and autosense");
      break;
    }
  }
  if (force_cdrom_sectors!=-1) {
    if(force_cdrom_sectors<0 || force_cdrom_sectors>100){
      report("Default sector read size must be 1<= n <= 100\n");
      cdda_close(d);
      d=NULL;
      exit(1);
    }
    {
      char buffer[256];
      sprintf(buffer,"Forcing default to read %d sectors; "
	      "ignoring preset and autosense",force_cdrom_sectors);
      report(buffer);
      d->nsectors=force_cdrom_sectors;
    }
  }
  if (force_cdrom_overlap!=-1) {
    if (force_cdrom_overlap<0 || force_cdrom_overlap>CDIO_CD_FRAMES_PER_SEC) {
      report("Search overlap sectors must be 0<= n <=75\n");
      cdda_close(d);
      d=NULL;
      exit(1);
    }
    {
      char buffer[256];
      sprintf(buffer,"Forcing search overlap to %d sectors; "
	      "ignoring autosense",force_cdrom_overlap);
      report(buffer);
    }
  }

  switch( cdda_open(d) ) {
  case -2:case -3:case -4:case -5:
    report("\nUnable to open disc.  Is there an audio CD in the drive?");
    exit(1);
  case -6:
    report("\nCdparanoia could not find a way to read audio from this drive.");
    exit(1);
  case 0:
    break;
  default:
    report("\nUnable to open disc.");
    exit(1);
  }

  d->i_test_flags = test_flags;

  /* Dump the TOC */
  if (query_only || verbose ) display_toc(d);

  if (query_only) exit(0);

  /* bias the disc.  A hack.  Of course. this is never the default. */
  /* 
     Some CD-ROM/CD-R drives will add an offset to the position on
     reading audio data. This is usually around 500-700 audio samples
     (ca. 1/75 second) on reading. So when this program queries a
     specific sector, it might not receive exactly that sector, but
     shifted by some amount.

     Note that if ripping includes the end of the CD, this will this
     cause this program to attempt to read partial sectors before or
     past the known user data area of the disc, probably causing read
     errors on most drives and possibly even hard lockups on some
     buggy hardware. 

     [Note to libcdio driver hackers: make sure all CD-drivers don't
     try to read outside of the stated disc boundaries.]
  */
  if(sample_offset){
    toc_offset+=sample_offset/588;
    sample_offset%=588;
    if(sample_offset<0){
      sample_offset+=588;
      toc_offset--;
    }
  }

  if (toc_bias) {
    toc_offset = -cdda_track_firstsector(d,1);
  }
  
  {
    int i;
    for( i=0; i < d->tracks+1; i++ )
      d->disc_toc[i].dwStartSector+=toc_offset;
  }

  if (force_cdrom_speed != -1) {
    cdda_speed_set(d,force_cdrom_speed);
  }

  if (d->nsectors==1) {
    report("WARNING: The autosensed/selected sectors per read value is\n"
	   "         one sector, making it very unlikely Paranoia can \n"
	   "         work.\n\n"
	   "         Attempting to continue...\n\n");
  }

  /* parse the span, set up begin and end sectors */

  {
    long i_first_lsn;
    long i_last_lsn;
    long batch_first;
    long batch_last;
    int batch_track;

    if (span) {
      /* look for the hyphen */ 
      char *span2=strchr(span,'-');
      if(strrchr(span,'-')!=span2){
	report("Error parsing span argument");
	exit(1);
      }
      
      if (span2!=NULL) {
	*span2='\0';
	span2++;
      }
      
      i_first_lsn=parse_offset(d, span, -1);

      if(i_first_lsn==-1)
	i_last_lsn=parse_offset(d, span2, cdda_disc_firstsector(d));

      else
	i_last_lsn=parse_offset(d, span2, i_first_lsn);
      
      if (i_first_lsn == -1) {
	if (i_last_lsn == -1) {
	  report("Error parsing span argument");
	  exit(1);
	} else {
	  i_first_lsn=cdda_disc_firstsector(d);
	}
      } else {
	if (i_last_lsn==-1) {
	  if (span2) { /* There was a hyphen */
	    i_last_lsn=cdda_disc_lastsector(d);
	  } else {
	    i_last_lsn=
	      cdda_track_lastsector(d,cdda_sector_gettrack(d, i_first_lsn));
	  }
	}
      }
    } else {
      i_first_lsn = cdda_disc_firstsector(d);
      i_last_lsn  = cdda_disc_lastsector(d);
    }

    {
      char buffer[250];
      int track1 = cdda_sector_gettrack(d, i_first_lsn);
      int track2 = cdda_sector_gettrack(d, i_last_lsn);
      long off1  = i_first_lsn - cdda_track_firstsector(d, track1);
      long off2  = i_last_lsn  - cdda_track_firstsector(d, track2);
      int i;

      for( i=track1; i<=track2; i++ )
	if(!cdda_track_audiop(d,i)){
	  report("Selected span contains non audio tracks.  Aborting.\n\n");
	  exit(1);
	}

      sprintf(buffer, "Ripping from sector %7ld (track %2d [%d:%02d.%02d])\n"
	      "\t  to sector %7ld (track %2d [%d:%02d.%02d])\n",
	      i_first_lsn,
	      track1,
	      (int) (off1/(CDIO_CD_FRAMES_PER_MIN)),
	      (int) ((off1/CDIO_CD_FRAMES_PER_SEC) % CDIO_CD_SECS_PER_MIN),
	      (int) (off1 % CDIO_CD_FRAMES_PER_SEC),
	      i_last_lsn,
	      track2,
	      (int) (off2/(CDIO_CD_FRAMES_PER_MIN)),
	      (int) ((off2/CDIO_CD_FRAMES_PER_SEC) % CDIO_CD_SECS_PER_MIN),
	      (int) (off2 % CDIO_CD_FRAMES_PER_SEC));
      report(buffer);
      
    }

    {
      long cursor;
      int16_t offset_buffer[1176];
      int offset_buffer_used=0;
      int offset_skip=sample_offset*4;

      p=paranoia_init(d);
      paranoia_modeset(p,paranoia_mode);
      if(force_cdrom_overlap!=-1)paranoia_overlapset(p,force_cdrom_overlap);

      if(verbose) {
	cdda_verbose_set(d,CDDA_MESSAGE_LOGIT,CDDA_MESSAGE_LOGIT);
	cdio_loglevel_default = CDIO_LOG_INFO;
      } else
	cdda_verbose_set(d,CDDA_MESSAGE_FORGETIT,CDDA_MESSAGE_FORGETIT);
      
      paranoia_seek(p,cursor=i_first_lsn,SEEK_SET);      

      /* this is probably a good idea in general */
      seteuid(getuid());
      setegid(getgid());

      /* we'll need to be able to read one sector past user data if we
	 have a sample offset in order to pick up the last bytes.  We
	 need to set the disc length forward here so that the libs are
	 willing to read past, assuming that works on the hardware, of
	 course */
      if(sample_offset)
	d->disc_toc[d->tracks].dwStartSector++;

      while(cursor<=i_last_lsn){
	char outfile_name[256];
	if ( batch ){
	  batch_first = cursor;
	  batch_track = cdda_sector_gettrack(d,cursor);
	  batch_last  = cdda_track_lastsector(d, batch_track);
	  if (batch_last>i_last_lsn) batch_last=i_last_lsn;
	} else {
	  batch_first = i_first_lsn;
	  batch_last  = i_last_lsn;
	  batch_track = -1;
	}
	
	callbegin=batch_first;
	callend=batch_last;
	
	/* argv[optind] is the span, argv[optind+1] (if exists) is outfile */
	
	if (optind+1<argc) {
	  if (!strcmp(argv[optind+1],"-") ){
	    out = dup(fileno(stdout));
	    if(batch)
	      report("Are you sure you wanted 'batch' "
		     "(-B) output with stdout?");
	    report("outputting to stdout\n");
	    outfile_name[0]='\0';
	  } else {
	    char path[256];

	    char *post=strrchr(argv[optind+1],'/');
	    int pos=(post?post-argv[optind+1]+1:0);
	    char *file=argv[optind+1]+pos;
	    
	    path[0]='\0';

	    if(pos)
	      strncat(path,argv[optind+1],pos>256?256:pos);

	    if(batch)
	      snprintf(outfile_name, 246, " %strack%02d.%s", path, 
		       batch_track, file);
	    else
	      snprintf(outfile_name, 246, "%s%s", path, file);

	    if(file[0]=='\0'){
	      switch (output_type) {
	      case 0: /* raw */
		strcat(outfile_name, "cdda.raw");
		break;
	      case 1:
		strcat(outfile_name, "cdda.wav");
		break;
	      case 2:
		strcat(outfile_name, "cdda.aifc");
		break;
	      case 3:
		strcat(outfile_name, "cdda.aiff");
		break;
	      }
	    }
	    
	    out=open(outfile_name,O_RDWR|O_CREAT|O_TRUNC,0666);
	    if(out==-1){
	      report3("Cannot open specified output file %s: %s",
		      outfile_name, strerror(errno));
	      exit(1);
	    }
	    report2("outputting to %s\n", outfile_name);
	  }
	} else {
	  /* default */
	  if (batch)
	    sprintf(outfile_name,"track%02d.", batch_track);
	  else
	    outfile_name[0]='\0';
	  
	  switch(output_type){
	  case 0: /* raw */
	    strcat(outfile_name,"cdda.raw");
	    break;
	  case 1:
	    strcat(outfile_name,"cdda.wav");
	    break;
	  case 2:
	    strcat(outfile_name,"cdda.aifc");
	    break;
	  case 3:
	    strcat(outfile_name,"cdda.aiff");
	    break;
	  }
	  
	  out = open(outfile_name, O_RDWR|O_CREAT|O_TRUNC, 0666);
	  if(out==-1){
	    report3("Cannot open default output file %s: %s", outfile_name,
		    strerror(errno));
	    exit(1);
	  }
	  report2("outputting to %s\n", outfile_name);
	}
	
	switch(output_type) {
	case 0: /* raw */
	  break;
	case 1: /* wav */
	  WriteWav(out, (batch_last-batch_first+1)*CDIO_CD_FRAMESIZE_RAW);
	  break;
	case 2: /* aifc */
	  WriteAifc(out, (batch_last-batch_first+1)*CDIO_CD_FRAMESIZE_RAW);
	  break;
	case 3: /* aiff */
	  WriteAiff(out, (batch_last-batch_first+1)*CDIO_CD_FRAMESIZE_RAW);
	  break;
	}
	
	/* Off we go! */

	if(offset_buffer_used){
	  /* partial sector from previous batch read */
	  cursor++;
	  if (buffering_write(out,
			      ((char *)offset_buffer)+offset_buffer_used,
			      CDIO_CD_FRAMESIZE_RAW-offset_buffer_used)){
	    report2("Error writing output: %s", strerror(errno));
	    exit(1);
	  }
	}
	
	skipped_flag=0;
	while(cursor<=batch_last){
	  /* read a sector */
	  int16_t *readbuf=paranoia_read_limited(p, callback, max_retries);
	  char *err=cdda_errors(d);
	  char *mes=cdda_messages(d);

	  if(mes || err)
	    fprintf(stderr,"\r                               "
		    "                                           \r%s%s\n",
		    mes?mes:"",err?err:"");
	  
	  if (err) free(err);
	  if (mes) free(mes);
	  if( readbuf==NULL) {
	    skipped_flag=1;
	    report("\nparanoia_read: Unrecoverable error, bailing.\n");
	    break;
	  }
	  if(skipped_flag && abort_on_skip){
	    cursor=batch_last+1;
	    break;
	  }

	  skipped_flag=0;
	  cursor++;
	  
	  if (output_endian!=bigendianp()) {
	    int i;
	    for (i=0; i<CDIO_CD_FRAMESIZE_RAW/2; i++)
	      readbuf[i]=UINT16_SWAP_LE_BE_C(readbuf[i]);
	  }
	  
	  callback(cursor*(CD_FRAMEWORDS)-1,-2);

	  if (buffering_write(out,((char *)readbuf)+offset_skip,
			     CDIO_CD_FRAMESIZE_RAW-offset_skip)){
	    report2("Error writing output: %s", strerror(errno));
	    exit(1);
	  }
	  offset_skip=0;
	  
	  if (output_endian != bigendianp()){
	    int i;
	    for (i=0; i<CDIO_CD_FRAMESIZE_RAW/2; i++)
	      readbuf[i] = UINT16_SWAP_LE_BE_C(readbuf[i]);
	  }

	  /* One last bit of silliness to deal with sample offsets */
	  if(sample_offset && cursor>batch_last){
	    int i;
	    /* read a sector and output the partial offset.  Save the
               rest for the next batch iteration */
	    readbuf=paranoia_read_limited(p,callback,max_retries);
	    err=cdda_errors(d);mes=cdda_messages(d);

	    if(mes || err)
	      fprintf(stderr,"\r                               "
		      "                                           \r%s%s\n",
		      mes?mes:"",err?err:"");
	  
	    if(err)free(err);if(mes)free(mes);
	    if(readbuf==NULL){
	      skipped_flag=1;
	      report("\nparanoia_read: Unrecoverable error reading through "
		     "sample_offset shift\n\tat end of track, bailing.\n");
	      break;
	    }
	    if (skipped_flag && abort_on_skip) break;
	    skipped_flag=0;
	    /* do not move the cursor */
	  
	    if(output_endian!=bigendianp())
	      for(i=0;i<CDIO_CD_FRAMESIZE_RAW/2;i++)
		offset_buffer[i]=UINT16_SWAP_LE_BE_C(readbuf[i]);
	    else
	      memcpy(offset_buffer,readbuf,CDIO_CD_FRAMESIZE_RAW);
	    offset_buffer_used=sample_offset*4;
	  
	    callback(cursor*(CD_FRAMEWORDS),-2);

	    if(buffering_write(out,(char *)offset_buffer,
			       offset_buffer_used)){
	      report2("Error writing output: %s", strerror(errno));
	      exit(1);
	    }
	  }
	}
	callback(cursor*(CDIO_CD_FRAMESIZE_RAW/2)-1,-1);
	buffering_close(out);
	if(skipped_flag){
	  /* remove the file */
	  report2("\nRemoving aborted file: %s", outfile_name);
	  unlink(outfile_name);
	  /* make the cursor correct if we have another track */
	  if(batch_track!=-1){
	    batch_track++;
	    cursor=cdda_track_firstsector(d,batch_track);
	    paranoia_seek(p,cursor, SEEK_SET);
	    offset_skip=sample_offset*4;
	    offset_buffer_used=0;
	  }
	}
	report("\n");
      }

      paranoia_free(p);
      p=NULL;
    }
  }

  report("Done.\n\n");

  return 0;
}
