/*
 * cdda_lib.c: The disc drive access functions
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: cdda_device.c,v 1.1.1.2 2005/06/27 13:34:02 cm Exp $
 */

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <vdr/tools.h>
#include <cdio/bytesex.h>
#include "cdda_device.h"
#include "cdda_cddb.h"

// --- cddaCdtext -------------------------------------------------------------

cCddaCdtext::cCddaCdtext() {
  p_title = p_performer = p_genre = p_message = p_composer = p_arranger = p_songwriter =  NULL;
  p_discid = p_isrc = p_tocinfo = p_tocinfo2 = NULL;
}

cCddaCdtext::~cCddaCdtext() {
  free(p_title);
  free(p_performer);
  free(p_genre);
  free(p_message);
  free(p_composer);
  free(p_arranger);
  free(p_songwriter);
  free(p_discid);
  free(p_isrc);
  free(p_tocinfo);
  free(p_tocinfo2);
}

const char *cCddaCdtext::SetTitle(const char *title) {
  if (NULL != p_title)
    free(p_title);

  if (NULL == title)
    return p_title = NULL;
  else
    return p_title = strdup(title);
}

const char *cCddaCdtext::SetPerformer(const char *performer) {
  if (NULL != p_performer)
    free(p_performer);

  if (NULL == performer)
    return p_performer = NULL;
  else
    return p_performer = strdup(performer);
}

const char *cCddaCdtext::SetGenre(const char *genre) {
  if (NULL != p_genre)
    free(p_genre);

  if (NULL == genre)
    return p_genre = NULL;
  else
    return p_genre = strdup(genre);
}

const char *cCddaCdtext::SetSongwriter(const char *songwriter) {
  if (NULL != p_songwriter)
    free(p_songwriter);

  if (NULL == songwriter)
    return p_songwriter = NULL;
  else
    return p_songwriter = strdup(songwriter);
}

const char *cCddaCdtext::SetArranger(const char *arranger) {
  if (NULL != p_arranger)
    free(p_arranger);

  if (NULL == arranger)
    return p_arranger = NULL;
  else
    return p_arranger = strdup(arranger);
}

const char *cCddaCdtext::SetComposer(const char *composer) {
  if (NULL != p_composer)
    free(p_composer);

  if (NULL == composer)
    return p_composer = NULL;
  else
    return p_composer = strdup(composer);
}

const char *cCddaCdtext::SetMessage(const char *message) {
  if (NULL != p_message)
    free(p_message);

  if (NULL == message)
    return p_message = NULL;
  else
    return p_message = strdup(message);
}

const char *cCddaCdtext::SetDiscId(const char *discid) {
  if (NULL != p_discid)
    free(p_discid);

  if (NULL == discid)
    return p_discid = NULL;
  else
    return p_discid = strdup(discid);
}

const char *cCddaCdtext::SetTocInfo(const char *tocinfo) {
  if (NULL != p_tocinfo)
    free(p_tocinfo);

  if (NULL == tocinfo)
    return p_tocinfo = NULL;
  else
    return p_tocinfo = strdup(tocinfo);
}

const char *cCddaCdtext::SetTocInfo2(const char *tocinfo2) {
  if (NULL != p_tocinfo2)
    free(p_tocinfo2);

  if (NULL == tocinfo2)
    return p_tocinfo2 = NULL;
  else
    return p_tocinfo2 = strdup(tocinfo2);
}

const char *cCddaCdtext::SetIsrc(const char *isrc) {
  if (NULL != p_isrc)
    free(p_isrc);

  if (NULL == isrc)
    return p_isrc = NULL;
  else
    return p_isrc = strdup(isrc);
}

// --- cddaDevice -------------------------------------------------------------
cCddaDevice::cCddaDevice() {
  p_device = NULL;
  p_cdIo = NULL;
  po_disc = NULL;

  cdio_init();
}

cCddaDevice::~cCddaDevice() {
  free(p_device);
}

int cCddaDevice::Open(const char* name) {
  if (NULL != (p_device = strdup(name))) {
    if (NULL == (p_cdIo = cdio_open(p_device, DRIVER_LINUX)))
      return CDDA_CANNOT_OPEN_DEVICE;
  } else
    return CDDA_OUT_OF_MEMORY;

  return CDDA_SUCCESS;
}

int cCddaDevice::Close(void) {
  if (po_disc)
    DELETENULL(po_disc);

  // cdio_destroy accepts NULL and should always be called
  cdio_destroy(p_cdIo);
  p_cdIo = NULL;

  return CDDA_SUCCESS;
}

cCddaDisc *cCddaDevice::Disc(void) {
  if (NULL == po_disc && NULL != p_cdIo)
    po_disc = new cCddaDisc(p_cdIo);

  return po_disc;
}

int cCddaDevice::Eject(void) {
  DELETENULL(po_disc);
  if (DRIVER_OP_SUCCESS != cdio_eject_media(&p_cdIo))
    return CDDA_CANNOT_EJECT_MEDIA;

  return CDDA_SUCCESS;
}

// --- cddaDisc ---------------------------------------------------------------
cCddaDisc::cCddaDisc(CdIo_t *cdIo) {
  cddbId = 0;
  cdtextDirty = false;
  po_tracks = NULL;
  po_cddb = NULL;
  p_cdIo = cdIo;
  discMode = CDIO_DISC_MODE_NO_INFO;
  po_cdtext = new cCddaCdtext();

  ReadCdtext();
}

cCddaDisc::~cCddaDisc() {
  delete po_cddb;
  delete po_cdtext;
  delete po_tracks;

  p_cdIo = NULL;
}

bool cCddaDisc::isAudio(void) {
  if (CDIO_DISC_MODE_NO_INFO == discMode)
    discMode = cdio_get_discmode(p_cdIo);

  return CDIO_DISC_MODE_CD_DA == discMode;
}

bool cCddaDisc::SetCdtextDirty(bool dirty) {
  return cdtextDirty = dirty;
}

cCddaTracks *cCddaDisc::Tracks(void) {
  if (NULL == po_tracks)
    po_tracks = new cCddaTracks(p_cdIo);

  return po_tracks;
}

unsigned int cCddaDisc::GetSum(unsigned int num) {
  unsigned int value = 0, n = num;

  while (0 < n) {
    value += n % 10;
    n /= 10;
  }

  return value;
}

void cCddaDisc::ReadCdtext(void) {
  cdtext_t *p_cdText = NULL;
  const char *p_string = NULL;
  int key = CDTEXT_ARRANGER;

  p_cdText = cdio_get_cdtext(p_cdIo, 0);

  if (NULL != p_cdText) {
    for (key = CDTEXT_ARRANGER; key < MAX_CDTEXT_FIELDS; key++) {
      if (NULL != p_cdText->field[key]) // FIX: workaround for bug in libcdio 0.72
        p_string = cdtext_get_const((cdtext_field_t)key, p_cdText);
      else
        continue;

      switch (key) {
        case CDTEXT_ARRANGER:
          po_cdtext->SetArranger(p_string);
          break;
        case CDTEXT_COMPOSER:
          po_cdtext->SetComposer(p_string);
          break;
        case CDTEXT_DISCID:
          po_cdtext->SetDiscId(p_string);
          break;
        case CDTEXT_GENRE:
          po_cdtext->SetGenre(p_string);
          break;
        case CDTEXT_MESSAGE:
          po_cdtext->SetMessage(p_string);
          break;
        case CDTEXT_ISRC:
          po_cdtext->SetIsrc(p_string);
          break;
        case CDTEXT_PERFORMER:
          po_cdtext->SetPerformer(p_string);
          break;
        case CDTEXT_SONGWRITER:
          po_cdtext->SetSongwriter(p_string);
          break;
        case CDTEXT_TITLE:
          po_cdtext->SetTitle(p_string);
          break;
        case CDTEXT_TOC_INFO:
          po_cdtext->SetTocInfo(p_string);
          break;
        case CDTEXT_TOC_INFO2:
          po_cdtext->SetTocInfo2(p_string);
          break;
        default:
          break;
      } // switch (key)
    } // for (...

    cdtext_destroy(p_cdText);
  }
}

unsigned long cCddaDisc::GetCddbId(void) {
  unsigned int sum = 0, i = 0;
  track_t firstTrack = 0;
  lba_t firstLba = 0;

  if (0 == cddbId) {
    if (CDIO_INVALID_TRACK == (firstTrack = cdio_get_first_track_num(p_cdIo)))
      return 0;

    if (CDIO_INVALID_LBA == (firstLba = cdio_get_track_lba(p_cdIo, firstTrack)))
      return 0;

    for (i = Tracks()->GetFirstNum(); i <= Tracks()->GetNumTracks(); i++)
      sum += GetSum(POSITIONTOSECONDS(Tracks()->Item(i)->GetOffset() + (firstLba / CDIO_CD_FRAMES_PER_SEC * 176400)));

    cddbId = ((sum % 0xff) << 24 | POSITIONTOSECONDS(Tracks()->GetLength()) << 8 | Tracks()->GetNumTracks());
  }

  return cddbId;
}

void cCddaDisc::ReadCddb(const char *fqdn, unsigned int port, const char *cacheDir) {
  unsigned int i = 0, *p_offsets = NULL;
  lba_t lba = 0;

  po_cddb = new cCddaCddb(this, fqdn, port, cacheDir);

  // po_cddb takes ownership of p_offsets
  p_offsets = (unsigned int *)malloc(sizeof(unsigned long) * Tracks()->GetNumTracks());
  if (NULL != p_offsets) {
    for (i = Tracks()->GetFirstNum(); i <= Tracks()->GetNumTracks(); i++) {
      //p_offsets[i - Tracks()->GetFirstNum()] = (Tracks()->Item(i)->GetOffset() / 176400 * CDIO_CD_FRAMES_PER_SEC) + firstLba;
      if (CDIO_INVALID_LBA == (lba = cdio_get_track_lba(p_cdIo, i))) {
        free(p_offsets);
        return;
      }

      p_offsets[i - Tracks()->GetFirstNum()] = lba;
    }

    if (0 == cddbId)
      GetCddbId();

    po_cddb->Query(cddbId, Tracks()->GetNumTracks(), POSITIONTOSECONDS(Tracks()->GetLength() + 2), p_offsets);
  }
}

void cCddaDisc::SetCdtext(cCddaCdtext *obj) {
  if (NULL != po_cdtext)
    delete po_cdtext;

  po_cdtext = obj;
}

// --- cddaTracks -------------------------------------------------------------

cCddaTracks::cCddaTracks(CdIo_t *cdIo) {
  p_cdIo = cdIo;
  numTracks = firstTrack = 0;
  memset(po_track, 0x00, sizeof(cCddaTrack *) * CDIO_CD_MAX_TRACKS);

  GetFirstNum();
  GetNumTracks();
}

cCddaTracks::~cCddaTracks() {
  for (int i = 0; i < CDIO_CD_MAX_TRACKS; i++)
    delete po_track[i];
  p_cdIo = NULL;
}

unsigned int cCddaTracks::GetNumTracks(void) {
  if (0 == numTracks)
    numTracks = cdio_get_last_track_num(p_cdIo) - cdio_get_first_track_num(p_cdIo) + 1;

  return numTracks;
}

unsigned int cCddaTracks::GetFirstNum(void) {
  if (0 == firstTrack)
    firstTrack = cdio_get_first_track_num(p_cdIo);

  return firstTrack;
}

cCddaTrack *cCddaTracks::Item(track_t index) {
  if (firstTrack <= index && index <= (firstTrack+numTracks)) {
    if (NULL == po_track[index])
      po_track[index] = new cCddaTrack(p_cdIo, index);

    return po_track[index];
  } else
    return NULL;
}

// returns the number of bytes
unsigned long cCddaTracks::GetLength(void) {
  lsn_t startLsn = 0, endLsn = 0;

  startLsn = cdio_get_track_lsn(p_cdIo, GetFirstNum());
  endLsn = cdio_get_track_last_lsn(p_cdIo, GetNumTracks());

  return (endLsn - startLsn) * CDIO_CD_FRAMESIZE_RAW;
}

// --- cddaTrack --------------------------------------------------------------
cCddaTrack::cCddaTrack(CdIo_t *cdIo, track_t index) {
  numChannels = 0;
  p_cdIo = cdIo;
  trackNum = index;
  readPosition = frameBuffLen = startLsn = endLsn = currentLsn = 0;
  trackFormat = TRACK_FORMAT_ERROR;
  po_cdtext = new cCddaCdtext();

  Initialize();
}

cCddaTrack::~cCddaTrack() {
  delete po_cdtext;

  p_cdIo = NULL;
}

void cCddaTrack::Initialize(void) {
  startLsn = currentLsn = cdio_get_track_lsn(p_cdIo, trackNum);
  endLsn = cdio_get_track_last_lsn(p_cdIo, trackNum);

  readPosition = (currentLsn - startLsn) * CDIO_CD_FRAMESIZE_RAW;

  ReadCdtext();
}

bool cCddaTrack::isAudio(void) {
  if (TRACK_FORMAT_ERROR == trackFormat)
    trackFormat = cdio_get_track_format(p_cdIo, trackNum);

  return TRACK_FORMAT_AUDIO == trackFormat;
}

void cCddaTrack::ReadCdtext(void) {
  cdtext_t *p_cdText = NULL;
  const char *p_string = NULL;
  int key = CDTEXT_ARRANGER;

  p_cdText = cdio_get_cdtext(p_cdIo, trackNum);

  if (NULL != p_cdText) {
    for (key = CDTEXT_ARRANGER; key < MAX_CDTEXT_FIELDS; key++) {
      if (NULL != p_cdText->field[key]) // FIX: workaround for bug in libcdio 0.72
        p_string = cdtext_get_const((cdtext_field_t)key, p_cdText);
      else
        continue;

      switch (key) {
        case CDTEXT_ARRANGER:
          po_cdtext->SetArranger(p_string);
          break;
        case CDTEXT_COMPOSER:
          po_cdtext->SetComposer(p_string);
          break;
        case CDTEXT_DISCID:
          po_cdtext->SetDiscId(p_string);
          break;
        case CDTEXT_GENRE:
          po_cdtext->SetGenre(p_string);
          break;
        case CDTEXT_MESSAGE:
          po_cdtext->SetMessage(p_string);
          break;
        case CDTEXT_ISRC:
          po_cdtext->SetIsrc(p_string);
          break;
        case CDTEXT_PERFORMER:
          po_cdtext->SetPerformer(p_string);
          break;
        case CDTEXT_SONGWRITER:
          po_cdtext->SetSongwriter(p_string);
          break;
        case CDTEXT_TITLE:
          po_cdtext->SetTitle(p_string);
          break;
        case CDTEXT_TOC_INFO:
          po_cdtext->SetTocInfo(p_string);
          break;
        case CDTEXT_TOC_INFO2:
          po_cdtext->SetTocInfo2(p_string);
          break;
        default:
          break;
      } // switch (key)
    } // for (...

    cdtext_destroy(p_cdText);
  }

}

// returns the number of bytes from the start of the cd
unsigned long cCddaTrack::GetOffset(void) {
  lsn_t lsn = 0;
  track_t track = 0;

  if (CDIO_INVALID_LSN == startLsn || CDIO_INVALID_LSN == endLsn)
    return 0;


  if (CDIO_INVALID_TRACK == (track = cdio_get_first_track_num(p_cdIo)))
    return 0;

  if (CDIO_INVALID_LSN == (lsn = cdio_get_track_lsn(p_cdIo, track)))
    return 0;
  else 
    return (startLsn - lsn) * CDIO_CD_FRAMESIZE_RAW;
}

// returns the number of bytes
unsigned long cCddaTrack::GetLength(void) {
  if (CDIO_INVALID_LSN == startLsn || CDIO_INVALID_LSN == endLsn)
    return 0;

  return (endLsn - startLsn) * CDIO_CD_FRAMESIZE_RAW;
}

int cCddaTrack::GetChannels(void) {
  if (!numChannels) {
    if (0 > (numChannels = cdio_get_track_channels(p_cdIo, trackNum)))
      return CDDA_FAIL;
  }
  return numChannels;
}

unsigned char *cCddaTrack::Read(int &length, long &position) {
  driver_return_code_t state = DRIVER_OP_SUCCESS;
  unsigned char *p_frame = NULL;
  int frameLength = 0;
  static int sectors = 0;

  if (CDIO_INVALID_LSN != startLsn && CDIO_INVALID_LSN != endLsn &&  (currentLsn < endLsn || (currentLsn == endLsn && frameBuffLen > 0))) {
    if (0 == frameBuffLen) {
      if (currentLsn + CDDA_SECTORS_MAX <= endLsn)
        sectors = CDDA_SECTORS_MAX;
      else {
        sectors = endLsn - currentLsn;
        memset(buffer + (sectors * CDIO_CD_FRAMESIZE_RAW) , 0x00, (CDDA_SECTORS_MAX - sectors) * CDIO_CD_FRAMESIZE_RAW);
      }

      state = cdio_read_audio_sectors(p_cdIo, buffer, currentLsn, sectors);
      if (DRIVER_OP_SUCCESS != state) {
        length = 0;
        position = CDDA_EOF;
        return NULL;
      } else {
        frameBuffLen = sectors * CDIO_CD_FRAMESIZE_RAW;
        currentLsn += sectors;
      }
    }
    if ((frameBuffLen - CDDA_AUDIO_FRAMESIZE) >= 0)
      frameLength = CDDA_AUDIO_FRAMESIZE;
    else
      frameLength = frameBuffLen;

    p_frame = buffer + (sectors * CDIO_CD_FRAMESIZE_RAW - frameBuffLen);
    length = frameLength;
    position = readPosition;

    readPosition += frameLength;
    frameBuffLen -= frameLength;
  } else {
    length = 0;
    position = CDDA_EOF;
    p_frame = NULL;
  }

  return p_frame;
}

// currently - seek works only on sector margins
// offset in bytes
// returns current position in bytes
long cCddaTrack::Seek(long offset, int whence) {
  lsn_t newLsn = 0;

  if (CDIO_INVALID_LSN == startLsn || CDIO_INVALID_LSN == endLsn) {
      return CDDA_INVALID_SEEK;
  }

  switch (whence) {
    case CDDA_SEEK_SET:
      newLsn = startLsn + (offset/CDIO_CD_FRAMESIZE_RAW);
      break;
    case CDDA_SEEK_END:
      newLsn = endLsn + (offset/CDIO_CD_FRAMESIZE_RAW);
      break;
    case CDDA_SEEK_CUR:
      newLsn = currentLsn + (offset/CDIO_CD_FRAMESIZE_RAW);
      break;
    default:
      return CDDA_INVALID_SEEK;
      break;
  }

  if (newLsn < startLsn)
    currentLsn = startLsn;
  else if (endLsn < currentLsn)
    currentLsn = endLsn;
  else
    currentLsn = newLsn;

  frameBuffLen = 0;
  return (readPosition = (currentLsn - startLsn) * CDIO_CD_FRAMESIZE_RAW);
}

void cCddaTrack::SetCdtext(cCddaCdtext *obj) {
  if (NULL != po_cdtext)
    delete po_cdtext;

  po_cdtext = obj;
}
