/* $Revision: 1.4 $ */
/*
 * This is libcdplay, a Linux-specific CD-ROM playing library. 
 *
 * This code is (C) 1998-2001 David E. Smith <dave@technopagan.org>
 * and released under the GNU GPL. See `COPYING' for details.
 *
 * (Old versions were under the LGPL. This is no longer the case.)
 *
 */

#include "libcdplay.h"
#include "sha.h"
#include "mbo.h"
#include "dcd.h"
#include "base64.h"
#include <unistd.h>
#include <string.h> 
#include <errno.h>
#include <fcntl.h>
#include <linux/cdrom.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>


#define DISC_MAGIC 42
static int cdrive;			/* fd for the cd, natch */
static int disc_present = DISC_MAGIC;	/* ugly hack for cd_present() */
static char discid[33];

static void int_cd_discid();

/* Plays just the one track specified by trknum. */
u_char cd_play_track (u_char trknum) {
  struct cdrom_ti trkidx;
  #ifdef DEBUG
    fprintf (stderr,"Entering cd_play_track (%i)\n", trknum);
  #endif
  if (!cd_present()) return -1;
  memset (&trkidx, 0, sizeof (struct cdrom_ti)); 
  trkidx.cdti_trk0 = trkidx.cdti_trk1 = trknum;
  if (cd_data_track(trknum)) return -1;
#ifdef OLD_STOP_BEHAVIOUR
  if(ioctl (cdrive, CDROMSTOP) == -1)
    fprintf(stderr, "CDROMSTOP failed: %s\n", strerror(errno));
#else
  if(ioctl (cdrive, CDROMSTART) == -1)
    fprintf(stderr, "CDROMSTART failed: %s\n", strerror(errno));
#endif
  if(ioctl (cdrive, CDROMPLAYTRKIND, &trkidx) == -1) {
    struct cdrom_tocentry toce0, toce1;
    struct cdrom_msf play;
    toce0.cdte_format = CDROM_MSF;
    toce0.cdte_track = trknum;
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce0) == -1)
      fprintf(stderr, "CDROMREADTOCENTRY[1] failed: %s\n", strerror(errno));
    toce1.cdte_format = CDROM_MSF;
    toce1.cdte_track = ((trknum == cd_last_track()) ? CDROM_LEADOUT : (trknum+1));
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce1) == -1) {
      fprintf(stderr, "CDROMREADTOCENTRY[2] (CDROM_LEADOUT) failed: %s\n",
       strerror(errno));
    }
    play.cdmsf_min0=toce0.cdte_addr.msf.minute;
    play.cdmsf_sec0=toce0.cdte_addr.msf.second;
    play.cdmsf_frame0=toce0.cdte_addr.msf.frame;
    play.cdmsf_min1=toce1.cdte_addr.msf.minute;
    play.cdmsf_sec1=toce1.cdte_addr.msf.second;
    play.cdmsf_frame1=toce1.cdte_addr.msf.frame;
    if(ioctl (cdrive, CDROMPLAYMSF, &play) == -1)
     fprintf(stderr, "CDROMPLAYMSF failed: %s\n", strerror(errno));
  }
  return trknum;
} /* cd_play_track */

/* Plays a sequential list of tracks, from trk1 to trk2. Unused. ;) */
u_char cd_play_sequence (u_char trk1, u_char trk2) {
  struct cdrom_ti trkidx;
  #ifdef DEBUG
    fprintf (stderr, "Entering cd_play_sequence (%i to %i)\n", trk1, trk2);
  #endif
  if (!cd_present()) return -1;
  memset (&trkidx, 0, sizeof (struct cdrom_ti));
  trkidx.cdti_trk0 = trk1;
  trkidx.cdti_trk1 = trk2;
#ifdef OLD_STOP_BEHAVIOUR
  if(ioctl (cdrive, CDROMSTOP) == -1)
    fprintf(stderr, "CDROMSTOP failed: %s\n", strerror(errno));
#else
  if(ioctl (cdrive, CDROMSTART) == -1)
    fprintf(stderr, "CDROMSTART failed: %s\n", strerror(errno));
#endif
  if(ioctl (cdrive, CDROMPLAYTRKIND, &trkidx) == -1) {
    struct cdrom_tocentry toce0, toce1;
    struct cdrom_msf play;
    toce0.cdte_format = CDROM_MSF;
    toce0.cdte_track = trk1;
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce0) == -1)
      fprintf(stderr, "CDROMREADTOCENTRY[1] failed: %s\n", strerror(errno));
    toce1.cdte_format = CDROM_MSF;
    toce1.cdte_track = ((trk2 == cd_last_track()) ? CDROM_LEADOUT : (trk2+1));
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce1) == -1) {
      fprintf(stderr, "CDROMREADTOCENTRY[2] (CDROM_LEADOUT) failed: %s\n",
       strerror(errno));
    }
    play.cdmsf_min0=toce0.cdte_addr.msf.minute;
    play.cdmsf_sec0=toce0.cdte_addr.msf.second;
    play.cdmsf_frame0=toce0.cdte_addr.msf.frame;
    play.cdmsf_min1=toce1.cdte_addr.msf.minute;
    play.cdmsf_sec1=toce1.cdte_addr.msf.second;
    play.cdmsf_frame1=toce1.cdte_addr.msf.frame;
    if(ioctl (cdrive, CDROMPLAYMSF, &play) == -1)
      fprintf(stderr, "CDROMPLAYMSF failed: %s\n", strerror(errno));
  }
  return trk2;
} /* play_seq */

/* Plays from trknum to the end of the disc */
u_char cd_play_disc (u_char trknum) {
  struct cdrom_ti trkidx;
  #ifdef DEBUG
    fprintf (stderr,"Entering cd_play_disc (%i)\n", trknum);
  #endif
  if (!cd_present()) return -1;
  memset (&trkidx, 0, sizeof(struct cdrom_ti));
  trkidx.cdti_trk0 = trknum;
  trkidx.cdti_trk1 = cd_last_track();
#ifdef OLD_STOP_BEHAVIOUR
  if(ioctl(cdrive, CDROMSTOP) == -1) /* has to be stopped to reset */
    fprintf(stderr, "CDROMSTOP failed: %s\n", strerror(errno)); 
#else
  if(ioctl(cdrive, CDROMSTART) == -1)
    fprintf(stderr, "CDROMSTART failed: %s\n", strerror(errno)); 
#endif
  if(ioctl(cdrive, CDROMPLAYTRKIND, &trkidx) == -1) {
    struct cdrom_tocentry toce0, toce1;
    struct cdrom_msf play;
    toce0.cdte_format = CDROM_MSF;
    toce0.cdte_track = trknum;
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce0) == -1)
      fprintf(stderr, "CDROMREADTOCENTRY[1] failed: %s\n", strerror(errno));
    toce1.cdte_format = CDROM_MSF;
    toce1.cdte_track = CDROM_LEADOUT;
    if(ioctl (cdrive, CDROMREADTOCENTRY, &toce1) == -1) {
      fprintf(stderr, "CDROMREADTOCENTRY[2] (CDROM_LEADOUT) failed: %s\n",
       strerror(errno));
    }
    play.cdmsf_min0=toce0.cdte_addr.msf.minute;
    play.cdmsf_sec0=toce0.cdte_addr.msf.second;
    play.cdmsf_frame0=toce0.cdte_addr.msf.frame;
    play.cdmsf_min1=toce1.cdte_addr.msf.minute;
    play.cdmsf_sec1=toce1.cdte_addr.msf.second;
    play.cdmsf_frame1=toce1.cdte_addr.msf.frame;
    if(ioctl (cdrive, CDROMPLAYMSF, &play) == -1)
      fprintf(stderr, "CDROMPLAYMSF failed: %s\n", strerror(errno));
  }
  return trknum;
} /* play_track */


/* This doesn't do what you think. That's why you shouldn't use it... */
int cd_active() {
  struct cdrom_subchnl subchnl;
  #if DEBUG > 1
    fprintf (stderr, "Entering cd_active()\n");
  #endif
  memset (&subchnl, '\0', sizeof (struct cdrom_subchnl));
  subchnl.cdsc_format=CDROM_MSF;
  if(ioctl (cdrive, CDROMSUBCHNL, &subchnl) == -1)
    fprintf(stderr, "CDROMSUBCHNL failed: %s\n", strerror(errno));
  #ifdef DEBUG
    fprintf (stderr, "subchannel status is %i\n", subchnl.cdsc_audiostatus);
  #endif
  switch (subchnl.cdsc_audiostatus) {
    case CDROM_AUDIO_INVALID:     return TRUE;    
    case CDROM_AUDIO_PLAY:
    case CDROM_AUDIO_PAUSED:      return TRUE; /* no, it's NOT a typo */
    default: return FALSE;
  }
  return FALSE;
}

/* return the track that's playing (or zero) */
u_char cd_current_track() {
  struct cdrom_subchnl subchnl;
  #ifdef DEBUG
    fprintf (stderr, "Calling cd_current_track()... ");
  #endif
  if (!cd_present()) return 0;
  memset (&subchnl, '\0', sizeof(struct cdrom_subchnl));
  subchnl.cdsc_format = CDROM_MSF;
  if(ioctl(cdrive, CDROMSUBCHNL, &subchnl) == -1)
    fprintf(stderr, "CDROMSUBCHNL failed: %s\n", strerror(errno));
  if (FALSE == cd_active()) return 0;
  #ifdef DEBUG
    fprintf (stderr, "returning %i\n", subchnl.cdsc_trk);
  #endif
  return subchnl.cdsc_trk;
} /* current_track */

unsigned long raw_track_length (u_char trknum) {
  long frames = 0;
  struct cdrom_tocentry toce;
  if (!cd_present()) return 0;
  toce.cdte_format = CDROM_MSF;
  toce.cdte_track = (trknum == cd_last_track()) ? CDROM_LEADOUT : trknum+1;
  if(ioctl (cdrive, CDROMREADTOCENTRY, &toce) == -1)
    fprintf(stderr, "CDROMREADTOCENTRY[3] failed: %s\n", strerror(errno));
  frames = (toce.cdte_addr.msf.minute) * CD_FRAMES * 60;
  frames += (toce.cdte_addr.msf.second) * CD_FRAMES;
  frames += toce.cdte_addr.msf.frame;
  return frames;
} /* raw */

unsigned long cd_track_length_frames (u_char trknum) {
  long frames = raw_track_length(trknum);
  long prevframes = raw_track_length (trknum-1);
  if (!cd_present()) return 0;
  frames -= prevframes;
  #if DEBUG > 1
    fprintf (stderr, "cd_track_length_frames: track %i is %lu frames\n",
             trknum, frames);
  #endif
  return frames;
}

/* returns length of track trknum, rounded to nearest second */
int cd_track_length (u_char trknum) {
  long frames = cd_track_length_frames(trknum);
  int seconds = frames / CD_FRAMES;
  if (!cd_present()) return 0;
  if ((frames % CD_FRAMES) > (CD_FRAMES / 2)) seconds++;
  #ifdef DEBUG
    fprintf (stderr, "cd_track_length: track %i is %i seconds\n",
             trknum, seconds);
  #endif
  return seconds; 
} /* track_length */

unsigned long cd_disc_length_frames (void) {
  long frames = raw_track_length(cd_last_track());
  #if DEBUG > 1
    fprintf (stderr, "cd_disc_length_frames: %lu\n", frames);
  #endif
  if (!cd_present()) return 0;
  return frames;
}

int cd_disc_length (void) {
  long frames = cd_disc_length_frames();
  int seconds = frames / CD_FRAMES;
  if (!cd_present()) return 0;
  if ((frames % CD_FRAMES) > (CD_FRAMES / 2)) seconds++;
  #ifdef DEBUG
    fprintf (stderr, "cd_disc_length: %i\n", seconds);
  #endif
  return seconds;
}

int cd_hw_status (void) {
  struct cdrom_subchnl cdsc;
  memset (&cdsc, '\0', sizeof (struct cdrom_subchnl));
  cdsc.cdsc_format = CDROM_MSF;
  if(ioctl (cdrive, CDROMSUBCHNL, &cdsc) == -1)
    fprintf(stderr, "CDROMSUBCHNL failed: %s\n", strerror(errno));
  return cdsc.cdsc_audiostatus;
}
/* The status codes are defined in cdrom.h. They may include:
   CDROM_AUDIO_PLAY (cd playing)
   CDROM_AUDIO_PAUSED
   CDROM_AUDIO_ERROR
   CDROM_AUDIO_COMPLETED (done with last request, drive stopped)
   CDROM_AUDIO_NO_STATUS (drive ready)

   Unless you know what you're doing, you probably don't want to use this.
*/

/* this was Othmar's, but I sorta reversed it. */
int cd_present(void) {
  int status;
  if (disc_present != DISC_MAGIC) status = disc_present;
    else {
      status = ioctl(cdrive, CDROM_DRIVE_STATUS);
      disc_present = status;
    }
  #ifdef DEBUG
    // fprintf (stderr, "cd_present(), status is %i\n", status);
  #endif
  if ((status == CDS_DISC_OK) || (status == CDS_NO_INFO)) return TRUE;
  /* returning TRUE if the drive doesn't support the status ioctl is,
     perhaps, risky, but one can only wonder... */
  /* other status codes: no disc, tray open, drive not ready */
  return FALSE;
}

/* toggle pause state */
void cd_pause (void) {
  int pause = cd_hw_status();
  #ifdef DEBUG
    fprintf (stderr, "Entering cd_pause. Current status is %i\n", pause);
  #endif
  switch (pause) {
    case CDROM_AUDIO_PAUSED:
      if(ioctl (cdrive, CDROMRESUME) == -1)
	fprintf(stderr, "CDROMRESUME failed: %s\n", strerror(errno));
      break;
    case CDROM_AUDIO_PLAY:
      if(ioctl (cdrive, CDROMPAUSE) == -1)
	fprintf(stderr, "CDROMPAUSE failed: %s\n", strerror(errno));
      break;
    default: cd_play_disc(cd_first_track()); break;
    /* the above was a really cool idea from Ronald Tol             */
    /* Any state other than CDROM_AUDIO_PLAY implies that we're not */
    /* playing right now, so what the hell? :) linux/cdrom.h lists  */
    /* all the possible states, and this seems to make sense...     */
  }
}


int cd_paused (void) {
  int pause = cd_hw_status();
  #ifdef DEBUG
    fprintf (stderr, "Entering cd_paused\n");
  #endif
  if (CDROM_AUDIO_PAUSED == pause) return TRUE;
  return FALSE;
}


void cd_stop (void) {
  #ifdef DEBUG
    fprintf (stderr, "Entering cd_stop()\n");
  #endif
  if(ioctl (cdrive, CDROMSTOP) == -1)
    fprintf(stderr, "CDROMSTOP failed: %s\n", strerror(errno));
}

void cd_eject (void) {
  #ifdef DEBUG
    fprintf (stderr, "Entering cd_eject()\n");
  #endif
  if (ioctl(cdrive,CDROM_DRIVE_STATUS) == CDS_TRAY_OPEN)
    ioctl(cdrive,CDROMCLOSETRAY);
    else ioctl (cdrive, CDROMEJECT);
}

u_char cd_first_track (void) {
  struct cdrom_tochdr tochdr;
  #ifdef DEBUG
    fprintf (stderr, "Calling cd_first_track()... ");
  #endif
  if (!cd_present()) return 0;
  if(ioctl (cdrive, CDROMREADTOCHDR, &tochdr) == -1)
    fprintf(stderr, "CDROMREADTOCHDR failed: %s\n", strerror(errno));
  #ifdef DEBUG
    fprintf (stderr, "returning %i\n", tochdr.cdth_trk0);
  #endif
  return tochdr.cdth_trk0;
}

u_char cd_last_track (void) {
  struct cdrom_tochdr tochdr;
  #ifdef DEBUG
    fprintf (stderr, "Calling cd_last_track()... ");
  #endif
  if (!cd_present()) return 0;
  if(ioctl (cdrive, CDROMREADTOCHDR, &tochdr) == -1)
    fprintf(stderr, "CDROMREADTOCHDR failed: %s\n", strerror(errno));
  #ifdef DEBUG
    fprintf (stderr, "returning %i\n", tochdr.cdth_trk1);
  #endif
  return tochdr.cdth_trk1;
}

int cd_data_track (u_char trknum) {
  struct cdrom_tocentry toce;
  int i;
  if (!cd_present()) return -1;
  memset (&toce, '\0', sizeof(struct cdrom_tocentry));
  toce.cdte_format = CDROM_MSF;
  toce.cdte_track  = trknum;
  ioctl(cdrive, CDROMREADTOCENTRY, &toce);
  /* if(ioctl (cdrive, CDROMREADTOCENTRY, &toce) == -1) */
    /* fprintf(stderr, "CDROMREADTOCENTRY[4] failed: %s\n", strerror(errno)); */
    /* apparently this fails randomly on some drives for no good reason. */
  i = (toce.cdte_ctrl & CDROM_DATA_TRACK ? TRUE : FALSE);
  #ifdef DEBUG
    fprintf (stderr, "toce.cdte_ctrl is %i\n", toce.cdte_ctrl);
    fprintf (stderr, "cd_data_track(%i) returning %i\n", trknum, i);
  #endif
  return i;
}
 
/* Hey, wow, it's the mandatory `initialize the drive' function. */
int cd_init_player (char *device) {
  #ifdef DEBUG
    fprintf(stderr, "Calling cd_init_player(%s). Debugging ON.\n", device);
  #endif

  cdrive = open(device, O_RDONLY | O_NONBLOCK);
  if (!cdrive) return -1;

  if (!cd_present()) return -1;

  int_cd_discid();
  mbo_init(device);  
  return cdrive;
}

/* never use this either. */
int cd_fd (void) {
  return cdrive;
}

/* Okay, this is just f'n silly. musicbrainz has a "Get CD Index"
 * function, but it won't work unless you've already retrieved
 * the information from the network. So I have to keep all the
 * "Get CD Index ID" code here.
 */

char *cd_discid(void) {
  return discid;
}

static void int_cd_discid (void) {
  SHA_INFO sha;
  unsigned char digest[20];
  unsigned long size;
  char temp[9];
  int i, lba;
  int trackz = cd_last_track();
  struct cdrom_tocentry toce;

  if (!cd_present()) return;;

  #if DEBUG > 1
    fprintf (stderr, "Entering cd_discid()\n");
  #endif

  memset (discid, '\0', sizeof(discid));
  memset (&toce, '\0', sizeof(toce));

  sha_init(&sha);
  sprintf (temp, "%02X", cd_first_track());
  sha_update(&sha, (unsigned char *)temp, strlen(temp));
  sprintf (temp, "%02X", cd_last_track());
  sha_update(&sha, (unsigned char *)temp, strlen(temp));

  toce.cdte_track = CDROM_LEADOUT;
  toce.cdte_format = CDROM_LBA;
  if(ioctl(cdrive, CDROMREADTOCENTRY, &toce) == -1)
    fprintf(stderr, "CDROMREADTOCENTRY[5] failed: %s\n", strerror(errno));
  lba = toce.cdte_addr.lba + 150;
  sprintf (temp, "%08X", lba);
  sha_update(&sha, (unsigned char *)temp, strlen(temp));
  
  for (i=1; i < 100; i++) {
    if (i <= trackz) {
      toce.cdte_format = CDROM_LBA;
      toce.cdte_track = i;
      if(ioctl (cdrive, CDROMREADTOCENTRY, &toce) == -1)
	fprintf(stderr, "CDROMREADTOCENTRY[6] failed: %s\n", strerror(errno));
      lba = toce.cdte_addr.lba + 150;
      sprintf(temp, "%08X", lba);
    }
    else sprintf (temp, "%08X", 0);
    sha_update(&sha, (unsigned char *)temp, strlen(temp));
  }
  sha_final(digest, &sha);
  #if DEBUG > 1
    fprintf (stderr, "SHA digest finalized.\n");
  #endif
  bin_to_base64(discid, digest, 20, size);
  #ifdef DEBUG
    fprintf (stderr, "cd_discid returning %s\n", discid);
  #endif
}

/* there used to be more stuff here, but it's been more-or-less
   subsumed by the musicbrainz crapulence. */

/* char *cd_discid(void); */
/* char *cd_subid(void);  */
