/* :ts=3
** ShowTOC - shows Table Of Content of an AudioCD in a readable and
**           freely configurable way
**
** written & © Ralph Reuchlein <amiga@rripley.de> 2000
**
** This software is eMailware, which means, that if you like this
** piece of software, you have to write me an eMail (address see end
** of readme).
*/

#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#include <exec/types.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include <clib/alib_protos.h>
#include <exec/memory.h>
#include <devices/scsidisk.h>

/* a 10 byte SCSI command */
typedef struct {
   UBYTE  opcode;
   UBYTE  b1;
   UBYTE  b2;
   UBYTE  b3;
   UBYTE  b4;
   UBYTE  b5;
   UBYTE  b6;
   UBYTE  b7;
   UBYTE  b8;
   UBYTE  control;
} SCSICMD10;

typedef struct {
   UBYTE pad0;
   UBYTE trackType;
   UBYTE trackNum;
   UBYTE pad1;
   ULONG startFrame;
} TOCENTRY;

typedef struct {
   UWORD    length;
   UBYTE    firstTrack;
   UBYTE    lastTrack;
   TOCENTRY tocs[100];
} TOC;

typedef struct {
   UBYTE mins;
   UBYTE secs;
   UBYTE frames;
   UBYTE fraction;
} TIMEINFO;

#define SCSI_CMD_READTOC   0x43
#define AUDIO_FRAMESIZE    2352
#define FRAMES_PER_SECOND  75
#define SECONDS_PER_MINUTE 60
#define SENSE_LEN          252

#define CDID_ARTIST_INDEX  0
#define CDID_TITLE_INDEX   1
#define CDID_TRACK_INDEX   2

const UBYTE ver_string[] = "$VER: ShowTOC 1.0 (08.01.00)Copyright © 2000 by Ralph Reuchlein";


/* ReadArgs defines ----------------------------------------------- */
#define ARGS_TEMPLATE   "DEVICE=D/K/A,"\
                        "UNIT=U/N/K/A,"\
                        "FORMAT=FMT/K,"\
                        "INTRODUCER=INTR/K,"\
                        "CDIDDIR/K,"\
                        "HEADFORMAT=HEADFMT/K,"\
                        "TAILFORMAT=TAILFMT/K,"\
                        "NOHEADER/S,"\
                        "NOLIST/S,"\
                        "NOTAIL/S"

enum {
   OPT_DEVICE = 0,
   OPT_UNIT,
   OPT_FMT,
   OPT_INTR,
   OPT_CDIDDIR,
   OPT_HEADFMT,
   OPT_TAILFMT,
   OPT_NOHEAD,
   OPT_NOLIST,
   OPT_NOTAIL,

   OPT_COUNT
};
static LONG args[OPT_COUNT];

#define ARG_DEVICE   (  (STRPTR)args[OPT_DEVICE])
#define ARG_UNIT     (*(ULONG *)args[OPT_UNIT])
#define ARG_FMT      (  (STRPTR)args[OPT_FMT])
#define ARG_HEADFMT  (  (STRPTR)args[OPT_HEADFMT])
#define ARG_TAILFMT  (  (STRPTR)args[OPT_TAILFMT])
#define ARG_INTR     (  (STRPTR)args[OPT_FMT])
#define ARG_CDIDDIR  (  (STRPTR)args[OPT_CDIDDIR])
#define ARG_NOHEAD   (args[OPT_NOHEAD]==-1)
#define ARG_NOLIST   (args[OPT_NOLIST]==-1)
#define ARG_NOTAIL   (args[OPT_NOTAIL]==-1)

#define IS_DEVICE    (args[OPT_DEVICE])
#define IS_UNIT      (args[OPT_UNIT])
#define IS_FMT       (args[OPT_FMT])
#define IS_HEADFMT   (args[OPT_HEADFMT])
#define IS_TAILFMT   (args[OPT_TAILFMT])
#define IS_INTR      (args[OPT_INTR])
#define IS_CDIDDIR   (args[OPT_CDIDDIR])
#define IS_NOHEAD    (args[OPT_NOHEAD]==-1)
#define IS_NOLIST    (args[OPT_NOLIST]==-1)
#define IS_NOTAIL    (args[OPT_NOTAIL]==-1)

#define DEFAULT_INTRODUCER             '%'

#define DEFAULT_HEAD                   (STRPTR)"       CD: %i%\\n    Title: %T%\\nArtist(s): %a"
#define DEFAULT_FORMAT                 (STRPTR)"Track  %2n: %d  %t"
#define DEFAULT_TAIL                   (STRPTR)"CD length: %f"

#define DEFAULT_TIME_START_FORMAT      (STRPTR)"%2sm:%02ss;%02sf"
#define DEFAULT_TIME_END_FORMAT        (STRPTR)"%2em:%02es;%02ef"
#define DEFAULT_TIME_DURATION_FORMAT   (STRPTR)"%2dm:%02ds;%02df"
#define DEFAULT_TIME_FULL_FORMAT       (STRPTR)"%2fm:%02fs;%02ff"

#define CDID_FORMAT                    (STRPTR)"ID%02d%06X%06X"



/* Prototypes ----------------------------------------------------- */
void     error(STRPTR,...);
TOC     *scanTOC(STRPTR,UBYTE);
void     getTimeInfo(TIMEINFO *,ULONG);
STRPTR  *readCDID(STRPTR);
void     freeCDID(STRPTR *);
void     printFmt(STRPTR,TOC *,STRPTR *,UBYTE,BOOL);

static BYTE io_error;

#define  BUF_SIZE    256
UBYTE buf[BUF_SIZE];


/* ---------------------------------------------------------------- */
void main(UWORD argc,STRPTR argv[]) {
   struct RDArgs *rdargs;
   TOC           *toc;
   STRPTR        *cdids=NULL;

   /* process arguments */
   if (rdargs=ReadArgs(ARGS_TEMPLATE,args,NULL)) {
      if ((toc=scanTOC(ARG_DEVICE,ARG_UNIT))) {
         register int i;
         UBYTE tracks=(toc->length/sizeof(TOCENTRY))-1;
         if (IS_CDIDDIR) {
            /* if CDIDDIR is specified, then build filename and try to
               read in CDID data */
            sprintf(buf,"%s",ARG_CDIDDIR);
            AddPart(buf,"",BUF_SIZE);
            sprintf(&buf[strlen(buf)],CDID_FORMAT,tracks,toc->tocs[2].startFrame,toc->tocs[tracks].startFrame);
            cdids=readCDID(buf);
         }
         if (!IS_NOHEAD)   printFmt((IS_HEADFMT)?(ARG_HEADFMT):(DEFAULT_HEAD),toc,cdids,0,TRUE);
         if (!IS_NOLIST)   for(i=0;i<tracks;i++) {
            printFmt((IS_FMT)?(ARG_FMT):(DEFAULT_FORMAT),toc,cdids,i,TRUE);
         }
         if (!IS_NOTAIL)   printFmt((IS_TAILFMT)?(ARG_TAILFMT):(DEFAULT_TAIL),toc,cdids,0,TRUE);
         if (cdids)  freeCDID(cdids);
         FreeVec((APTR)toc);
      }
      else {
         error("Error accessing %s, unit %i: error $%02x\n"
               "(maybe no CD-ROM drive or no media?)\n",ARG_DEVICE,ARG_UNIT,io_error);
      }
      FreeArgs(rdargs);
   }
   else {
      error("Usage: %s %s\n",argv[0],ARGS_TEMPLATE);
   }
}



/* ---------------------------------------------------------------- */
void error(STRPTR fmt,...) {
   va_list a;
   va_start(a,fmt);
   vfprintf(stdout,fmt,a);
   va_end(a);
}



/* ---------------------------------------------------------------- */

TOC *scanTOC(STRPTR scsiname,UBYTE unit) {
   static struct MsgPort  *msgPort;
   static struct IOStdReq *ioReq;
   static struct SCSICmd   scsicmd;
   static SCSICMD10       *scsi;
   static TOC             *toc;
   static UBYTE           *sense;
   BOOL                    ok=FALSE;

   if ((scsi = (SCSICMD10 *)AllocVec(sizeof(SCSICMD10),MEMF_CLEAR|MEMF_CHIP))) {
      if ((toc = (TOC *)AllocVec(sizeof(TOC),MEMF_CLEAR|MEMF_CHIP))) {
         if ((sense = (UBYTE *)AllocVec(SENSE_LEN,MEMF_CLEAR|MEMF_CHIP))) {
            /* create MsgPort */
            if ((msgPort=CreatePort(NULL,0))!=NULL) {
               /* create IOStdReq */
               if ((ioReq=CreateStdIO(msgPort))!=NULL) { 
                  /* open the SCSI device */
                  if (!OpenDevice(scsiname,unit,(struct IORequest *)ioReq,0)) {
                     /* fill IOStdReq */
                     ioReq->io_Length  = sizeof(struct SCSICmd);
                     ioReq->io_Data    = (APTR)&scsicmd;
                     ioReq->io_Command = HD_SCSICMD;
                     /* fill SCSICmd */
                     scsicmd.scsi_Data          = (APTR)toc;
                     scsicmd.scsi_Length        = sizeof(TOC);
                     scsicmd.scsi_Command       = (APTR)scsi;
                     scsicmd.scsi_CmdLength     = sizeof(SCSICMD10);
                     scsicmd.scsi_Flags         = (SCSIF_READ | SCSIF_AUTOSENSE);
                     scsicmd.scsi_SenseData     = (APTR)sense;
                     scsicmd.scsi_SenseLength   = SENSE_LEN;
                     scsicmd.scsi_SenseActual   = 0;
                     /* fill scsi */
                     scsi->opcode      = SCSI_CMD_READTOC;
                     scsi->b1          = 0;
                     scsi->b2          = 0;
                     scsi->b3          = 0;
                     scsi->b4          = 0;
                     scsi->b5          = 0;
                     scsi->b6          = 0;
                     scsi->b7          = (sizeof(TOC)>>8);
                     scsi->b8          = (sizeof(TOC)&0xFF);
                     scsi->control     = 0;

                     /* do the request */
                     if (DoIO((struct IORequest *)ioReq)==0) {;
                        ok=TRUE;
                     }
                     io_error = ioReq->io_Error;

                     /* close device */
                     CloseDevice((struct IORequest *)ioReq);
                     /* and remove all needed resources */
                  }
                  DeleteExtIO((struct IORequest *)ioReq);
               }
               DeletePort(msgPort);
            }
            FreeVec((APTR)sense);
         }
         /* do not free TOC, because we need the data */
      }
      FreeVec((APTR)scsi);
   }
   if (ok) {
      return toc;
   }
   else {
      FreeVec((APTR)toc);
   }
   return NULL;
}


/* ---------------------------------------------------------------- */

void getTimeInfo(TIMEINFO *ti,ULONG frames) {
   ti->frames = frames % FRAMES_PER_SECOND;
   ti->fraction = (UBYTE)(100 * (double)ti->frames / (double)FRAMES_PER_SECOND + 0.5);
   frames /= FRAMES_PER_SECOND;
   ti->secs = (UBYTE)(frames % SECONDS_PER_MINUTE);
   frames /= SECONDS_PER_MINUTE;
   ti->mins = (UBYTE) frames;
}



/* ---------------------------------------------------------------- */

STRPTR *readCDID(STRPTR filename) {
   ULONG    filelen=-1;
   FILE    *fh;
   STRPTR   cdid;
   STRPTR  *cdids=NULL;

   /* try to examine file length */
   struct FileInfoBlock *fib=AllocDosObject(DOS_FIB,NULL);
   if (fib) {
      BPTR l;
      /* lock the file */
      if (l=Lock(filename,ACCESS_READ)) {
         /* examine file */
         if (Examine(l,fib)) {
            filelen=fib->fib_Size;
         }
         else {
            error("%s: Couldn't examine file information!\n",filename);
         }
         UnLock(l);
      }
      else {
         /*error("%s: Couldn't access to this file (maybe not found?)!\n",filename);*/
      }
      FreeDosObject(DOS_FIB,fib);
   }
   else {
      error("%s: Couldn't allocate FileInfoBlock memory!\n",filename);
   }
   /* file length couldn't be examined (file not found etc.), return */
   if (filelen==-1)  return NULL;

   /* read in CDID file and store line pointers */
   if (fh=fopen(filename,"r")) {
      if (cdid=AllocVec(filelen+1,MEMF_CLEAR|MEMF_REVERSE)) {
         if (fread(cdid,1,filelen,fh)==filelen) {
            register STRPTR cdidtmp=cdid;
            register UWORD  lines=0;
            cdid[filelen]=0;     /* terminate string */
            /* scan the buffer for newlines */
            while (*cdidtmp) {
               if (*(cdidtmp++)=='\n')  lines++;
            }
            /* allocate pointer list */
            if (cdids=(STRPTR *)AllocVec((lines+1)*sizeof(STRPTR),MEMF_CLEAR|MEMF_REVERSE)) {
               /* find line starts and store their pointers */
               cdids[0]=cdid;
               cdidtmp=cdid; lines=1;
               while (*cdidtmp) {
                  if (*cdidtmp=='\n') {
                     *cdidtmp=0;
                     cdids[lines++]=(cdidtmp+1);
                  }
                  cdidtmp++;
               }
               cdids[lines]=NULL;
               /* cdids not freed! */
            }
            else {
               error("Couldn't allocate %i bytes for CDID pointer data.\n",(lines+1)*sizeof(STRPTR));
            }
         }
         else {
            error("%s: Reading error on CDID file!\n",filename);
         }
         /* cdid not freed! */
      }
      else {
         error("Couldn't allocate %i bytes for CDID data.\n",filelen+1);
      }
      fclose(fh);
   }
   else {
      error("%s: Couldn't open CDID file for reading!\n",filename);
   }
   return cdids;
}

void freeCDID(STRPTR *cdids) {
   if (cdids) {
      FreeVec((APTR)cdids[0]);
      FreeVec((APTR)cdids);
   }
}



/* ---------------------------------------------------------------- */
#define FMT_STRING      's'
#define FMT_INTEGER     'i'

void printFmt(STRPTR fmt,TOC *toc,STRPTR *cdids,UBYTE trackNum,BOOL newLine) {
   register int tmp0,tmp1,intr;
   STRPTR   fmt_end = fmt + strlen(fmt),tmp_fmt,str2;
   TIMEINFO ti;
   ULONG    frames;
   UBYTE    tracks=(toc->length/sizeof(TOCENTRY))-1;

   /* if there is no format, return */
   if (!fmt) return;

   /* if toc is too small, return */
   if (toc->length <= 6) return;

   /* do we have specified a special introducer? */
   if (ARG_INTR) {
      intr = *ARG_INTR;    /* only the first character */
   }
   else {
      intr = DEFAULT_INTRODUCER;
   }

   /* we process the format string: replace the format types by
      C-format types and process the format portion one by one */
   tmp_fmt=fmt;
   while(fmt<fmt_end) {
      tmp_fmt=fmt;
      /* search for the introducer */
      while ((*fmt!=intr) && (fmt<fmt_end)) fmt++; if (fmt>=fmt_end) break;
      /* write string til introducer */
      *fmt=0; fputs(tmp_fmt,stdout); *fmt='%';     /* fprintf() only uses '%'! */
      tmp_fmt=fmt;
      /* search for the terminating format type character */
      while ((*fmt!='\\') && (!isalpha(*fmt)) && (fmt<fmt_end)) fmt++; if (fmt>=fmt_end) break;
      tmp0 = *fmt; tmp1=*(fmt+1);
      switch (tmp0) {
         case '\\':     /* special characters */
            switch (tmp1) {
               case 'n':   fputs("\n",stdout); break;
               case 'r':   fputs("\r",stdout); break;
               case 'f':   fputs("\f",stdout); break;
               case 't':   fputs("\t",stdout); break;
            }
            fmt+=2;
            break;
         case 's':      /* start time of track */
         case 'e':      /* end time of track */
         case 'd':      /* duration of track */
         case 'f':      /* full CD time */
            switch (tmp0) {
               case 's':
                  frames=toc->tocs[trackNum].startFrame;
                  str2=DEFAULT_TIME_START_FORMAT;
                  break;
               case 'e':
                  frames=toc->tocs[trackNum+1].startFrame;
                  str2=DEFAULT_TIME_END_FORMAT;
                  break;
               case 'd':
                  frames=toc->tocs[trackNum+1].startFrame-toc->tocs[trackNum].startFrame;
                  str2=DEFAULT_TIME_DURATION_FORMAT;
                  break;
               case 'f':
                  frames=toc->tocs[tracks].startFrame;
                  str2=DEFAULT_TIME_FULL_FORMAT;
                  break;
               default:
                   frames=0; str2="";   /* should never be reached! */
            }
            getTimeInfo(&ti,frames);
            switch(tmp1) {
               case 'm':
                  *fmt=FMT_INTEGER; *(fmt+1)=0;
                  fprintf(stdout,tmp_fmt,ti.mins);
                  *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                  break;
               case 's':
                  *fmt=FMT_INTEGER; *(fmt+1)=0;
                  fprintf(stdout,tmp_fmt,ti.secs);
                  *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                  break;
               case 'f':
                  *fmt=FMT_INTEGER; *(fmt+1)=0;
                  fprintf(stdout,tmp_fmt,ti.frames);
                  *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                  break;
               case 'd':
                  *fmt=FMT_INTEGER; *(fmt+1)=0;
                  fprintf(stdout,tmp_fmt,ti.fraction);
                  *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                  break;
               case 'a':
                  *fmt=FMT_INTEGER; *(fmt+1)=0;
                  fprintf(stdout,tmp_fmt,frames);
                  *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                  break;
               case 'n':
                  if ((tmp0=='s') || (tmp0=='e')) {
                     *fmt=FMT_INTEGER; *(fmt+1)=0;
                     fprintf(stdout,tmp_fmt,(tmp0=='s')?(toc->firstTrack):(toc->lastTrack));
                     *fmt=tmp0; fmt++; *fmt=tmp1; fmt++;
                     break;
                  }
               default:
                  printFmt(str2,toc,cdids,trackNum,FALSE);
                  fmt++;
            }
            break;

         case 'n':   /* track number */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,toc->tocs[trackNum].trackNum);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'N':   /* number of tracks */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,tracks);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'b':   /* track length in bytes */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            frames=toc->tocs[trackNum+1].startFrame-toc->tocs[trackNum].startFrame;
            fprintf(stdout,tmp_fmt,frames*AUDIO_FRAMESIZE);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'y':   /* type of track */
            *fmt=FMT_STRING; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,(toc->tocs[trackNum].trackType & 0x04) ? "Data" : "Audio");
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'c':   /* number of channels in track as numeric value */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,(toc->tocs[trackNum].trackType & 0x08) ? 4 : 2);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'C':   /* number of channels in track as text */
            *fmt=FMT_STRING; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,(toc->tocs[trackNum].trackType & 0x08) ? "four" : "two");
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'p':   /* Is digital copy of track permitted or prohibited? */
            *fmt=FMT_STRING; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,(toc->tocs[trackNum].trackType & 0x02) ? "permitted" : "prohibited");
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'P':   /* Is digital copy of track permitted or prohibited? */
            *fmt=FMT_STRING; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,(toc->tocs[trackNum].trackType & 0x02) ? "y" : "n");
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'i':   /* CD ID */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            fprintf(stdout,CDID_FORMAT,tracks,toc->tocs[2].startFrame,toc->tocs[tracks].startFrame);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'l':   /* TOC length on CD */
            *fmt=FMT_INTEGER; *(fmt+1)=0;
            fprintf(stdout,tmp_fmt,toc->length);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'T':   /* Title of CD (only if CDID file is found/provided) */
            *fmt=FMT_STRING; *(fmt+1)=0;
            if (cdids)  fprintf(stdout,tmp_fmt,cdids[CDID_TITLE_INDEX]);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 't':   /* Title of track (only if CDID file is found/provided) */
            *fmt=FMT_STRING; *(fmt+1)=0;
            if (cdids)  fprintf(stdout,tmp_fmt,cdids[CDID_TRACK_INDEX+trackNum]);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         case 'a':   /* Artist of CD (only if CDID file is found/provided) */
            *fmt=FMT_STRING; *(fmt+1)=0;
            if (cdids)  fprintf(stdout,tmp_fmt,cdids[CDID_ARTIST_INDEX]);
            *fmt=tmp0; fmt++; *fmt=tmp1;
            break;

         default:
            *(fmt+1)=0;
            fputs(tmp_fmt,stdout);
            fmt++; *fmt=tmp1;
      }
      tmp_fmt=fmt;
   }
   if (tmp_fmt<fmt) {
      fputs(tmp_fmt,stdout);
   }
   if (newLine)   fputc('\n',stdout);
}
