/*
 *
 * $Id: k3bcdparanoialib.cpp 621693 2007-01-09 14:38:25Z trueg $
 * Copyright (C) 2003-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * This file is part of the K3b project.
 * Copyright (C) 1998-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * 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 of the License, or
 * (at your option) any later version.
 * See the file "COPYING" for the exact licensing terms.
 */

#include <config.h>

#include "k3bcdparanoialib.h"

#include <k3bdevice.h>
#include <k3btoc.h>
#include <k3bmsf.h>

#include <kdebug.h>

#include <dlfcn.h>

#include <tqfile.h>
#include <tqmutex.h>


static bool s_haveLibCdio = false;


void* K3bCdparanoiaLib::s_libInterface = 0;
void* K3bCdparanoiaLib::s_libParanoia = 0;
int K3bCdparanoiaLib::s_counter = 0;


#define CDDA_IDENTIFY          s_haveLibCdio ? "cdio_cddap_identify" : "cdda_identify"
#define CDDA_CLOSE             s_haveLibCdio ? "cdio_cddap_close" : "cdda_close"
#define CDDA_OPEN              s_haveLibCdio ? "cdio_cddap_open" : "cdda_open"
#define CDDA_TRACK_FIRSTSECTOR s_haveLibCdio ? "cdio_cddap_track_firstsector" : "cdda_track_firstsector"
#define CDDA_TRACK_LASTSECTOR  s_haveLibCdio ? "cdio_cddap_track_lastsector" : "cdda_track_lastsector"
#define CDDA_VERBOSE_SET       s_haveLibCdio ? "cdio_cddap_verbose_set" : "cdda_verbose_set"
#define CDDA_DISC_FIRSTSECTOR  s_haveLibCdio ? "cdio_cddap_disc_firstsector" : "cdda_disc_firstsector"

#define PARANOIA_INIT          s_haveLibCdio ? "cdio_paranoia_init" : "paranoia_init"
#define PARANOIA_FREE          s_haveLibCdio ? "cdio_paranoia_free" : "paranoia_free"
#define PARANOIA_MODESET       s_haveLibCdio ? "cdio_paranoia_modeset" : "paranoia_modeset"
#define PARANOIA_SEEK          s_haveLibCdio ? "cdio_paranoia_seek" : "paranoia_seek"
#define PARANOIA_READ_LIMITED  s_haveLibCdio ? "cdio_paranoia_read_limited" : "paranoia_read_limited"


// from cdda_paranoia.h
#define PARANOIA_CB_READ           0
#define PARANOIA_CB_VERIFY         1
#define PARANOIA_CB_FIXUP_EDGE     2
#define PARANOIA_CB_FIXUP_ATOM     3
#define PARANOIA_CB_SCRATCH        4
#define PARANOIA_CB_REPAIR         5
#define PARANOIA_CB_SKIP           6
#define PARANOIA_CB_DRIFT          7
#define PARANOIA_CB_BACKOFF        8
#define PARANOIA_CB_OVERLAP        9
#define PARANOIA_CB_FIXUP_DROPPED 10
#define PARANOIA_CB_FIXUP_DUPED   11
#define PARANOIA_CB_READERR       12



static void paranoiaCallback( long, int status )
{
  // do nothing so far....
  return;

  switch( status ) {
  case -1:
    break;
  case -2:
    break;
  case PARANOIA_CB_READ:
    // no problem
    // does only this mean that the sector has been read?
//     m_lastReadSector = sector;  // this seems to be rather useless
//     m_readSectors++;
    break;
  case PARANOIA_CB_VERIFY:
    break;
  case PARANOIA_CB_FIXUP_EDGE:
    break;
  case PARANOIA_CB_FIXUP_ATOM:
    break;
  case PARANOIA_CB_SCRATCH:
    // scratch detected
    break;
  case PARANOIA_CB_REPAIR:
    break;
  case PARANOIA_CB_SKIP:
    // skipped sector
    break;
  case PARANOIA_CB_DRIFT:
    break;
  case PARANOIA_CB_BACKOFF:
    break;
  case PARANOIA_CB_OVERLAP:
    // sector does not seem to contain the current
    // sector but the amount of overlapped data
    //    m_overlap = sector;
    break;
  case PARANOIA_CB_FIXUP_DROPPED:
    break;
  case PARANOIA_CB_FIXUP_DUPED:
    break;
  case PARANOIA_CB_READERR:
    break;
  } 
}



extern "C" {
  struct cdrom_drive;
  struct cdrom_paranoia;

  // HINT: these pointers must NOT have the same name like the actual methods!
  //       I added "cdda_" as prefix
  //       Before doing that K3b crashed in cdda_open!
  //       Can anyone please explain that to me?

  // cdda_interface
  cdrom_drive* (*cdda_cdda_identify)(const char*, int, char**);
  int (*cdda_cdda_open)(cdrom_drive *d);
  int (*cdda_cdda_close)(cdrom_drive *d);
  long (*cdda_cdda_track_firstsector)( cdrom_drive*, int );
  long (*cdda_cdda_track_lastsector)( cdrom_drive*, int );
  long (*cdda_cdda_disc_firstsector)(cdrom_drive *d);
  void (*cdda_cdda_verbose_set)(cdrom_drive *d,int err_action, int mes_action);

  // cdda_paranoia
  cdrom_paranoia* (*cdda_paranoia_init)(cdrom_drive*);
  void (*cdda_paranoia_free)(cdrom_paranoia *p);
  void (*cdda_paranoia_modeset)(cdrom_paranoia *p, int mode);
  int16_t* (*cdda_paranoia_read_limited)(cdrom_paranoia *p, void(*callback)(long,int), int);
  long (*cdda_paranoia_seek)(cdrom_paranoia *p,long seek,int mode);
}

// from cdda_paranoia.h
#define PARANOIA_MODE_FULL        0xff
#define PARANOIA_MODE_DISABLE     0

#define PARANOIA_MODE_VERIFY      1
#define PARANOIA_MODE_FRAGMENT    2
#define PARANOIA_MODE_OVERLAP     4
#define PARANOIA_MODE_SCRATCH     8
#define PARANOIA_MODE_REPAIR      16
#define PARANOIA_MODE_NEVERSKIP   32



/**
 * Internal class used by K3bCdparanoiaLib
 */
class K3bCdparanoiaLibData
{
 public:
  K3bCdparanoiaLibData( K3bDevice::Device* dev )
    : m_device(dev),
      m_drive(0),
      m_paranoia(0),
      m_currentSector(0) {
    s_dataMap.insert( dev, this );
  }

  ~K3bCdparanoiaLibData() {
    paranoiaFree();

    s_dataMap.erase( m_device );
  }

  K3bDevice::Device* device() const { return m_device; }
  void paranoiaModeSet( int );
  bool paranoiaInit();
  void paranoiaFree();
  int16_t* paranoiaRead( void(*callback)(long,int), int maxRetries );
  long paranoiaSeek( long, int );
  long firstSector( int );
  long lastSector( int );
  long sector() const { return m_currentSector; }

  static K3bCdparanoiaLibData* data( K3bDevice::Device* dev ) {
    TQMap<K3bDevice::Device*, K3bCdparanoiaLibData*>::const_iterator it = s_dataMap.find( dev );
    if( it == s_dataMap.constEnd() )
      return new K3bCdparanoiaLibData( dev );
    else
      return *it;
  }

  static void freeAll() {
    // clean up all K3bCdparanoiaLibData instances
    for( TQMap<K3bDevice::Device*, K3bCdparanoiaLibData*>::iterator it = s_dataMap.begin();
	 it != s_dataMap.end(); ++it )
      delete it.data();
  }

 private:
  //
  // We have exactly one instance of K3bCdparanoiaLibData per device
  //
  static TQMap<K3bDevice::Device*, K3bCdparanoiaLibData*> s_dataMap;

  K3bDevice::Device* m_device;

  cdrom_drive* m_drive;
  cdrom_paranoia* m_paranoia;

  long m_currentSector;

  TQMutex mutex;
};


TQMap<K3bDevice::Device*, K3bCdparanoiaLibData*> K3bCdparanoiaLibData::s_dataMap;

bool K3bCdparanoiaLibData::paranoiaInit()
{
  mutex.lock();

  if( m_drive )
    paranoiaFree();

  // since we use cdparanoia to open the device it is important to close
  // the device here
  m_device->close();

  m_drive = cdda_cdda_identify( TQFile::encodeName(m_device->blockDeviceName()), 0, 0 );
  if( m_drive == 0 ) {
    mutex.unlock();
    return false;
  }

  //  cdda_cdda_verbose_set( m_drive, 1, 1 );

  cdda_cdda_open( m_drive );
  m_paranoia = cdda_paranoia_init( m_drive );
  if( m_paranoia == 0 ) {
    mutex.unlock();
    paranoiaFree();
    return false;
  }

  m_currentSector = 0;

  mutex.unlock();

  return true;
}


void K3bCdparanoiaLibData::paranoiaFree()
{
  mutex.lock();

  if( m_paranoia ) {
    cdda_paranoia_free( m_paranoia );
    m_paranoia = 0;
  }
  if( m_drive ) {
    cdda_cdda_close( m_drive );
    m_drive = 0;
  }

  mutex.unlock();
}


void K3bCdparanoiaLibData::paranoiaModeSet( int mode )
{
  mutex.lock();
  cdda_paranoia_modeset( m_paranoia, mode );
  mutex.unlock();
}


int16_t* K3bCdparanoiaLibData::paranoiaRead( void(*callback)(long,int), int maxRetries )
{
  if( m_paranoia ) {
    mutex.lock();
    int16_t* data = cdda_paranoia_read_limited( m_paranoia, callback, maxRetries );
    if( data )
      m_currentSector++;
    mutex.unlock();
    return data;
  }
  else
    return 0;
}


long K3bCdparanoiaLibData::firstSector( int track )
{
  if( m_drive ) {
    mutex.lock();
    long sector = cdda_cdda_track_firstsector( m_drive, track );
    mutex.unlock();
    return sector;
  }
  else
    return -1;
}

long K3bCdparanoiaLibData::lastSector( int track )
{
  if( m_drive ) {
    mutex.lock();
    long sector = cdda_cdda_track_lastsector(m_drive, track );
    mutex.unlock();
    return sector;
  }
  else
   return -1;
}


long K3bCdparanoiaLibData::paranoiaSeek( long sector, int mode )
{
  if( m_paranoia ) {
    mutex.lock();
    m_currentSector = cdda_paranoia_seek( m_paranoia, sector, mode );
    mutex.unlock();
    return m_currentSector;
  }
  else
    return -1;
}



class K3bCdparanoiaLib::Private
{
public:
  Private() 
    : device(0),
      currentSector(0),
      startSector(0),
      lastSector(0),
      status(S_OK),
      paranoiaLevel(0),
      neverSkip(true),
      maxRetries(5),
      data(0) {
  }

  ~Private() {
  }

  void updateParanoiaMode() {
    // from cdrdao 1.1.7
    int paranoiaMode = PARANOIA_MODE_FULL^PARANOIA_MODE_NEVERSKIP;

    switch( paranoiaLevel ) {
    case 0:
      paranoiaMode = PARANOIA_MODE_DISABLE;
      break;

    case 1:
      paranoiaMode |= PARANOIA_MODE_OVERLAP;
      paranoiaMode &= ~PARANOIA_MODE_VERIFY;
      break;

    case 2:
      paranoiaMode &= ~(PARANOIA_MODE_SCRATCH|PARANOIA_MODE_REPAIR);
      break;
    }

    if( neverSkip )
      paranoiaMode |= PARANOIA_MODE_NEVERSKIP;

    data->paranoiaModeSet( paranoiaMode );
  }

  // high-level api
  K3bDevice::Device* device;
  K3bDevice::Toc toc;
  long currentSector;
  long startSector;
  long lastSector;
  int status;
  unsigned int currentTrack;
  int paranoiaLevel;
  bool neverSkip;
  int maxRetries;

  K3bCdparanoiaLibData* data;
};


K3bCdparanoiaLib::K3bCdparanoiaLib()
{
  d = new Private();
  s_counter++;
}


K3bCdparanoiaLib::~K3bCdparanoiaLib()
{
  delete d;
  s_counter--;
  if( s_counter == 0 ) {
    K3bCdparanoiaLibData::freeAll();

    // cleanup the dynamically loaded lib
    dlclose( s_libInterface );
    dlclose( s_libParanoia );
    s_libInterface = 0;
    s_libParanoia = 0;
  }
}


bool K3bCdparanoiaLib::load()
{
  cdda_cdda_identify = (cdrom_drive* (*) (const char*, int, char**))dlsym( s_libInterface, CDDA_IDENTIFY );
  cdda_cdda_open = (int (*) (cdrom_drive*))dlsym( s_libInterface, CDDA_OPEN );
  cdda_cdda_close = (int (*) (cdrom_drive*))dlsym( s_libInterface, CDDA_CLOSE );
  cdda_cdda_track_firstsector = (long (*)(cdrom_drive*, int))dlsym( s_libInterface, CDDA_TRACK_FIRSTSECTOR );
  cdda_cdda_track_lastsector = (long (*)(cdrom_drive*, int))dlsym( s_libInterface, CDDA_TRACK_LASTSECTOR );
  cdda_cdda_verbose_set = (void (*)(cdrom_drive *d,int err_action, int mes_action))dlsym( s_libInterface, CDDA_VERBOSE_SET );
  cdda_cdda_disc_firstsector = (long (*)(cdrom_drive *d))dlsym( s_libInterface, CDDA_DISC_FIRSTSECTOR );

  cdda_paranoia_init = (cdrom_paranoia* (*)(cdrom_drive*))dlsym( s_libParanoia, PARANOIA_INIT );
  cdda_paranoia_free = (void (*)(cdrom_paranoia *p))dlsym( s_libParanoia, PARANOIA_FREE );
  cdda_paranoia_modeset = (void (*)(cdrom_paranoia *p, int mode))dlsym( s_libParanoia, PARANOIA_MODESET );
  cdda_paranoia_read_limited = (int16_t* (*)(cdrom_paranoia *p, void(*callback)(long,int), int))dlsym( s_libParanoia, PARANOIA_READ_LIMITED );
  cdda_paranoia_seek = (long (*)(cdrom_paranoia *p,long seek,int mode))dlsym( s_libParanoia, PARANOIA_SEEK );

  // check if all symbols could be resoled
  if( cdda_cdda_identify == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_identify'" << endl;
    return false;
  }
  if( cdda_cdda_open == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_open'" << endl;
    return false;
  }
  if( cdda_cdda_close == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_close'" << endl;
    return false;
  }
  if( cdda_cdda_track_firstsector == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_track_firstsector'" << endl;
    return false;
  }
  if( cdda_cdda_track_lastsector == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_track_lastsector'" << endl;
    return false;
  }
  if( cdda_cdda_disc_firstsector == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_disc_firstsector'" << endl;
    return false;
  }
  if( cdda_cdda_verbose_set == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'cdda_verbose_set'" << endl;
    return false;
  }

  if( cdda_paranoia_init == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'paranoia_init'" << endl;
    return false;
  }
  if( cdda_paranoia_free == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'paranoia_free'" << endl;
    return false;
  }
  if( cdda_paranoia_modeset == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'paranoia_modeset'" << endl;
    return false;
  }
  if( cdda_paranoia_read_limited == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'paranoia_read_limited'" << endl;
    return false;
  }
  if( cdda_paranoia_seek == 0 ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve 'paranoia_seek'" << endl;
    return false;
  }

  return true;
}



K3bCdparanoiaLib* K3bCdparanoiaLib::create()
{
  // check if libcdda_interface is avalilable
  if( s_libInterface == 0 ) {
    s_haveLibCdio = false;

    s_libInterface = dlopen( "libcdda_interface.so.0", RTLD_NOW|RTLD_GLOBAL );

    // try the redhat & Co. location
    if( s_libInterface == 0 )
      s_libInterface = dlopen( "cdda/libcdda_interface.so.0", RTLD_NOW|RTLD_GLOBAL );

    // try the new cdio lib
    if( s_libInterface == 0 ) {
      s_libInterface = dlopen( "libcdio_cdda.so", RTLD_NOW|RTLD_GLOBAL );
      s_haveLibCdio = true;
    }

    if( s_libInterface == 0 ) {
      kdDebug() << "(K3bCdparanoiaLib) Error while loading libcdda_interface. " << endl;
      return 0;
    }


    s_libParanoia = dlopen( "libcdda_paranoia.so.0", RTLD_NOW );

    // try the redhat & Co. location
    if( s_libParanoia == 0 )
      s_libParanoia = dlopen( "cdda/libcdda_paranoia.so.0", RTLD_NOW );

    // try the new cdio lib
    if( s_haveLibCdio && s_libParanoia == 0 )
      s_libParanoia = dlopen( "libcdio_paranoia.so.0", RTLD_NOW );

    if( s_libParanoia == 0 ) {
      kdDebug() << "(K3bCdparanoiaLib) Error while loading libcdda_paranoia. " << endl;
      dlclose( s_libInterface );
      s_libInterface = 0;
      return 0;
    }
  }

  K3bCdparanoiaLib* lib = new K3bCdparanoiaLib();
  if( !lib->load() ) {
    kdDebug() << "(K3bCdparanoiaLib) Error: could not resolve all symbols!" << endl;
    delete lib;
    return 0;
  }
  return lib;
}


bool K3bCdparanoiaLib::initParanoia( K3bDevice::Device* dev, const K3bDevice::Toc& toc )
{
  if( !dev ) {
    kdError() << "(K3bCdparanoiaLib::initParanoia) dev = 0!" << endl;
    return false;
  }

  close();

  d->device = dev;
  d->toc = toc;
  if( d->toc.isEmpty() ) {
    kdDebug() << "(K3bCdparanoiaLib) empty toc." << endl;
    cleanup();
    return false;
  }

  if( d->toc.contentType() == K3bDevice::DATA ) {
    kdDebug() << "(K3bCdparanoiaLib) No audio tracks found." << endl;
    cleanup();
    return false;
  }

  //
  // Get the appropriate data instance for this device
  //
  d->data = K3bCdparanoiaLibData::data( dev );

  if( d->data->paranoiaInit() ) {
    d->startSector = d->currentSector = d->lastSector = 0;

    return true;
  }
  else {
    cleanup();
    return false;
  }
}


bool K3bCdparanoiaLib::initParanoia( K3bDevice::Device* dev )
{
  return initParanoia( dev, dev->readToc() );
}


void K3bCdparanoiaLib::close()
{
  cleanup();
}


void K3bCdparanoiaLib::cleanup()
{
  if( d->data )
    d->data->paranoiaFree();
  d->device = 0;
  d->currentSector = 0;
}


bool K3bCdparanoiaLib::initReading()
{
  if( d->device ) {
    // find first audio track
    K3bDevice::Toc::const_iterator trackIt = d->toc.begin();
    while( (*trackIt).type() != K3bDevice::Track::AUDIO ) {
      ++trackIt;
    }

    long start = (*trackIt).firstSector().lba();

    // find last audio track
    while( trackIt != d->toc.end() && (*trackIt).type() == K3bDevice::Track::AUDIO )
      ++trackIt;
    --trackIt;

    long end = (*trackIt).lastSector().lba();

    return initReading( start, end );
  }
  else {
    kdDebug() << "(K3bCdparanoiaLib) initReading without initParanoia." << endl;
    return false;
  }
}


bool K3bCdparanoiaLib::initReading( unsigned int track )
{
  if( d->device ) {
    if( track <= d->toc.count() ) {
      const K3bDevice::Track& k3bTrack = d->toc[track-1];
      if( k3bTrack.type() == K3bDevice::Track::AUDIO ) {
	return initReading( k3bTrack.firstSector().lba(), k3bTrack.lastSector().lba() );
      }
      else {
	kdDebug() << "(K3bCdparanoiaLib) Track " << track << " no audio track." << endl;
	return false;
      }
    }
    else {
      kdDebug() << "(K3bCdparanoiaLib) Track " << track << " too high." << endl;
      return false;
    }
  }
  else {
    kdDebug() << "(K3bCdparanoiaLib) initReading without initParanoia." << endl;
    return false;
  }
}


bool K3bCdparanoiaLib::initReading( long start, long end )
{
  kdDebug() << "(K3bCdparanoiaLib) initReading( " << start << ", " << end << " )" << endl;

  if( d->device ) {
    if( d->toc.firstSector().lba() <= start &&
	d->toc.lastSector().lba() >= end ) {
      d->startSector = d->currentSector = start;
      d->lastSector = end;

      // determine track number
      d->currentTrack = 1;
      while( d->toc[d->currentTrack-1].lastSector() < start )
	d->currentTrack++;

      // let the paranoia stuff point to the startSector
      d->data->paranoiaSeek( start, SEEK_SET );
      return true;
    }
    else {
      kdDebug() << "(K3bCdparanoiaLib) " << start << " and " << end << " out of range." << endl;
      return false;
    }
  }
  else {
    kdDebug() << "(K3bCdparanoiaLib) initReading without initParanoia." << endl;
    return false;
  }
}


char* K3bCdparanoiaLib::read( int* statusCode, unsigned int* track, bool littleEndian )
{
  if( d->currentSector > d->lastSector ) {
    kdDebug() << "(K3bCdparanoiaLib) finished ripping. read " 
	      << (d->currentSector - d->startSector) << " sectors." << endl
	      << "                   current sector: " << d->currentSector << endl;
    d->status = S_OK;
    if( statusCode )
      *statusCode = d->status;
    return 0;
  }

  if( d->currentSector != d->data->sector() ) {
    kdDebug() << "(K3bCdparanoiaLib) need to seek before read. Looks as if we are reusing the paranoia instance." << endl;
    if( !d->data->paranoiaSeek( d->currentSector, SEEK_SET ) )
      return 0;
  }

  //
  // The paranoia data could have been used by someone else before
  // and setting the paranoia mode is fast
  //
  d->updateParanoiaMode();

  TQ_INT16* data = d->data->paranoiaRead( paranoiaCallback, d->maxRetries );

  char* charData = reinterpret_cast<char*>(data);

#ifdef WORDS_BIGENDIAN // __BYTE_ORDER == __BIG_ENDIAN
  if( littleEndian ) {
#else
  if( !littleEndian ) {
#endif
    for( int i = 0; i < CD_FRAMESIZE_RAW-1; i+=2 ) {
      char b = charData[i];
      charData[i] = charData[i+1];
      charData[i+1] = b;
    }
  }


  if( data )
    d->status = S_OK;
  else
    d->status = S_ERROR; // We may skip this sector if we'd like...

  if( statusCode )
    *statusCode = d->status;

  if( track )
    *track = d->currentTrack;

  d->currentSector++;

  if( d->toc[d->currentTrack-1].lastSector() < d->currentSector )
    d->currentTrack++;

  return charData;
}


int K3bCdparanoiaLib::status() const
{
  return d->status;
}


const K3bDevice::Toc& K3bCdparanoiaLib::toc() const
{
  return d->toc;
}


long K3bCdparanoiaLib::rippedDataLength() const
{
  return d->lastSector - d->startSector + 1;
}


void K3bCdparanoiaLib::setParanoiaMode( int m )
{
  d->paranoiaLevel = m;
}


void K3bCdparanoiaLib::setNeverSkip( bool b )
{
  d->neverSkip = b;
}


void K3bCdparanoiaLib::setMaxRetries( int r )
{
  d->maxRetries = r;
}