/* ADCD - A Diminutive CD player for Linux/GNU
   Copyright (C) 2004-2026 Antonio Diaz Diaz.

   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.

   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, see <http://www.gnu.org/licenses/>.
*/

#include <cstdio>
#include <cstdlib>
#include <vector>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/cdrom.h>

#include "msf_time.h"
#include "cd.h"


CD::CD( const char * const filename )
  : status( no_disc ),
    track_( 0 ), first_track_( 0 ), last_track_( 0 ), index_( 0 ), loop_( 0 ),
    linear_( true )
  {
  filedes = ::open( filename, O_RDONLY | O_NONBLOCK );
  if( filedes < 0 )
    {
    std::fprintf( stderr, "adcd: cannot open '%s' in non-blocking mode.\n", filename );
    std::fprintf( stderr, "Please, check whether you have permission to read device '%s'\n", filename );
    exit( 1 );
    }
  read_status();
  }


CD::~CD() { if ( filedes >= 0 ) ::close( filedes ); }


bool CD::read_status( const bool force )
  {
  if( !force && status != playing ) return false;
  const Status old_status = status;
  const int old_tracks = tracks();

  int drive_status;
  const int timeout = 15;		// my drive needs 8 seconds
  for( int i = 0; i < timeout; ++i )
    {
    drive_status = ioctl( filedes, CDROM_DRIVE_STATUS );
    if( !force || drive_status != CDS_DRIVE_NOT_READY || i + 1 >= timeout )
      break;
    sleep( 1 );
    }
  switch( drive_status )
    {
    case CDS_NO_INFO:
    case CDS_DISC_OK:
      if( status == no_disc || status == stopped )
        {
        struct cdrom_tochdr tinfo;
        if( ioctl( filedes, CDROMREADTOCHDR, &tinfo ) == 0 )
          {
          first_track_ = tinfo.cdth_trk0; last_track_ = tinfo.cdth_trk1;
          if( tracks() )
            {
            timelist.resize( tracks() );
            int i;
            for( i = 0; i <= tracks(); ++i )
              {
              struct cdrom_tocentry entry;
              entry.cdte_track = (i < tracks()) ? i + first_track_ : CDROM_LEADOUT;
              entry.cdte_format = CDROM_MSF;
              if( ioctl( filedes, CDROMREADTOCENTRY, &entry ) != 0 ) break;
              int m = entry.cdte_addr.msf.minute;
              int s = entry.cdte_addr.msf.second;
              int f = entry.cdte_addr.msf.frame;
              if( i > 0 ) timelist[i-1].end = Msf_time( m, s, f - 1 );
              if( i < tracks() ) timelist[i].start = Msf_time( m, s, f );
              }
            if( i > tracks() )
              {
              if( track_ < first_track_ ) track_ = first_track_;
              else if( track_ > last_track_ ) track_ = last_track_;
              time_abs = time_start( track_ ); time_rel.reset();
              status = stopped;
              }
            }
          }
        }
      break;
    case CDS_NO_DISC:
    case CDS_TRAY_OPEN:
    case CDS_DRIVE_NOT_READY:
    default                 : status = no_disc;
    }
  if( status != no_disc )
    {
    struct cdrom_subchnl ch;
    ch.cdsc_format = CDROM_MSF;
    if( ioctl( filedes, CDROMSUBCHNL, &ch ) != 0 ) status = no_disc;
    else switch( ch.cdsc_audiostatus )
      {
      case CDROM_AUDIO_PLAY:
        status = playing;
        if( loop_ != 2 ) track_ = ch.cdsc_trk;
        time_abs = Msf_time( ch.cdsc_absaddr.msf.minute,
                             ch.cdsc_absaddr.msf.second,
                             ch.cdsc_absaddr.msf.frame );
        time_rel = Msf_time( ch.cdsc_reladdr.msf.minute,
                             ch.cdsc_reladdr.msf.second,
                             ch.cdsc_reladdr.msf.frame );
        break;
      case CDROM_AUDIO_PAUSED   : status = paused; break;
      case CDROM_AUDIO_INVALID  :
      case CDROM_AUDIO_NO_STATUS:
      case CDROM_AUDIO_COMPLETED:
        if( status == playing )
          {
          if( loop_ == 2 || ( linear_ && time_rel <= Msf_time( 0, 2 ) ) ) play();
          else if( !next_track() ) status = stopped;
          }
        break;
      case CDROM_AUDIO_ERROR: status = stopped; break;
      }
    }
  if( status == no_disc && old_status != no_disc )
    {
    track_ = first_track_ = last_track_ = 0;
    time_abs.reset(); time_rel.reset(); timelist.clear();
    }
  if( tracks() != old_tracks )
    { index_ = 0; linear_ = true; playlist_.clear(); }
  return ( status != no_disc || status != old_status || tracks() != old_tracks );
  }


void CD::close()
  {
  if( status == no_disc ) { ioctl( filedes, CDROMCLOSETRAY ); read_status(); }
  }


void CD::open()
  {
  stop(); ioctl( filedes, CDROMEJECT ); read_status();
  }


void CD::pause()
  {
  if( status == playing ) { ioctl( filedes, CDROMPAUSE ); status = paused; }
  else if( status == paused )
    { ioctl( filedes, CDROMRESUME ); status = playing; read_status(); }
  }


void CD::play()
  {
  if( status == paused ) { pause(); return; }
  if( status == stopped ) read_status();
  if( status == no_disc ) { close(); if( status == no_disc ) return; }

  struct cdrom_ti ti;
  ti.cdti_ind0 = ti.cdti_ind1 = 1;			// FIXME Linus
  if( loop_ == 2 ) ti.cdti_trk0 = ti.cdti_trk1 = track_;
  else if( linear_ ) { ti.cdti_trk0 = track_; ti.cdti_trk1 = last_track_; }
  else if( playlist_.size() )
    {
    if( index_ >= playlist_.size() ) index_ = playlist_.size() - 1;
    ti.cdti_trk0 = ti.cdti_trk1 = track_ = playlist_[index_];
    }
  else return;
  if( !tracks() || track_ < first_track_ || track_ > last_track_ ) return;
  ioctl( filedes, CDROMPLAYTRKIND, &ti );
  read_status();
  }


void CD::stop()
  {
  if( status == playing || status == paused )
    { ioctl( filedes, CDROMSTOP ); status = stopped; }
  }


bool CD::next_track()
  {
  if( linear_ )
    {
    if( track_ >= first_track_ - 1 && track_ < last_track_ ) ++track_;
    else if( loop_ ) track_ = first_track_;
    else return false;
    }
  else if( playlist_.size() )
    {
    if( index_ < playlist_.size() - 1 ) track_ = playlist_[++index_];
    else if( loop_ ) { index_ = 0; track_ = playlist_[index_]; }
    else return false;
    }
  else return false;
  return track( track_ );
  }


bool CD::prev_track()
  {
  if( linear_ )
    {
    if( track_ > first_track_ && track_ <= last_track_ + 1 ) --track_;
    else if( loop_ ) track_ = last_track_;
    else return false;
    }
  else if( playlist_.size() )
    {
    if( index_ > 0 ) track_ = playlist_[--index_];
    else if( loop_ )
      { index_ = playlist_.size() - 1; track_ = playlist_[index_]; }
    else return false;
    }
  else return false;
  return track( track_ );
  }


bool CD::seek_forward( const int seconds )
  {
  if( status != playing || timelist.empty() ||
      ( !linear_ && playlist_.empty() ) ) return false;

  Msf_time target = time_abs + Msf_time( 0, seconds );
  Msf_time end = timelist.back().end;
  if( !linear_ || loop_ == 2 ) end = time_end( track_ );
  if( target >= end )
    {
    if( ( !linear_ || loop_ ) && next_track() ) return true;
    else target = end - Msf_time( 0, 0, 1 );
    }
  struct cdrom_msf msf;
  msf.cdmsf_min0 = target.minute();
  msf.cdmsf_sec0 = target.second();
  msf.cdmsf_frame0 = target.frame();
  msf.cdmsf_min1 = end.minute();
  msf.cdmsf_sec1 = end.second();
  msf.cdmsf_frame1 = end.frame();
  ioctl( filedes, CDROMPLAYMSF, &msf );
  read_status();
  return true;
  }


bool CD::seek_backward( const int seconds )
  {
  if( status != playing || timelist.empty() ||
      ( !linear_ && playlist_.empty() ) ) return false;

  Msf_time target = time_abs - Msf_time( 0, seconds );
  Msf_time start = timelist.front().start;
  Msf_time end = timelist.back().end;
  if( !linear_ || loop_ == 2 )
    { start = time_start( track_ ); end = time_end( track_ ); }
  if( target < start )
    {
    if( ( !linear_ || loop_ ) && prev_track() ) return true;
    if( linear_ ) track_ = first_track_;
    time_abs = start; time_rel.reset(); stop(); return true;
    }
  struct cdrom_msf msf;
  msf.cdmsf_min0 = target.minute();
  msf.cdmsf_sec0 = target.second();
  msf.cdmsf_frame0 = target.frame();
  msf.cdmsf_min1 = end.minute();
  msf.cdmsf_sec1 = end.second();
  msf.cdmsf_frame1 = end.frame();
  ioctl( filedes, CDROMPLAYMSF, &msf );
  read_status();
  return true;
  }


void CD::loop( const int new_loop )
  {
  loop_ = (new_loop < 0) ? 0 : new_loop % 3;
  if( loop_ == 2 && linear_ && status == playing ) seek_forward( 1 );
  }


const char * CD::loop_name() const
  {
  switch( loop_ )
    {
    case 0 : return "No   ";
    case 1 : return "Disc ";
    case 2 : return "Track";
    default: return "error";
    }
  }


void CD::playlist( const std::vector< int > & pl )
  {
  playlist_ = pl;
  if( status == stopped || playlist_.empty() ) { index_ = 0; return; }
  for( unsigned i = 0; i < playlist_.size(); ++i )
    if( playlist_[i] == track_ ) { index_ = i; return; }
  if( index_ >= playlist_.size() ) index_ = playlist_.size() - 1;
  }


void CD::show_info() const
  {
  switch( status )
    {
    case no_disc: std::fputs( "No Disc\n", stdout ); break;
    case stopped: std::fputs( "Stopped\n", stdout ); break;
    case paused : std::fputs( "Paused\n", stdout ); break;
    case playing:
      {
      Msf_time msf = time( relative ), total = time_track( track_ );
      std::printf( "Playing track %d / %d -- time %d:%02d / %d:%02d -- volume %d\n",
                   track_, last_track_, msf.minute(), msf.second(),
                   total.minute(), total.second(), volume() );
      } break;
    default:      std::fputs( "Unknown\n", stdout );
    }
  }


const char * CD::status_name() const
  {
  switch( status )
    {
    case no_disc: return "No Disc";
    case stopped: return "Stopped";
    case paused : return "Paused";
    case playing: return "Playing";
    default:      return "Unknown";
    }
  }


bool CD::track( const int new_track, const bool start )
  {
  if( start )
    {
    if( status == stopped ) read_status();
    if( status == no_disc ) { close(); if( status == no_disc ) return false; }
    }
  if( !tracks() || new_track < first_track_ || new_track > last_track_ )
    return false;

  track_ = new_track;
  if( !linear_ && playlist_.size() && playlist_[index_] != track_ )
    {
    for( unsigned i = index_ + 1; i < playlist_.size(); ++i )
      if( track_ == playlist_[i] ) { index_ = i; break; }
    if( playlist_[index_] != track_ )
      for( unsigned i = 0; i < playlist_.size(); ++i )
        if( track_ == playlist_[i] ) { index_ = i; break; }
    if( playlist_[index_] != track_ ) linear_ = true;
    }
  if( status == paused || start ) status = playing;
  if( status == playing ) play();
  else { time_abs = time_start( track_ ); time_rel.reset(); }
  return true;
  }


int CD::volume() const
  {
  struct cdrom_volctrl volctrl;
  if( ioctl( filedes, CDROMVOLREAD, &volctrl ) == 0 )
    return ( volctrl.channel0 + volctrl.channel1 ) / 2;
  else return 0;
  }


bool CD::volume( int vol ) const
  {
  if( vol < 0 ) vol = 0; else if( vol > 255 ) vol = 255;
  struct cdrom_volctrl volctrl;
  volctrl.channel0 = volctrl.channel1 = vol;
  return ( ioctl( filedes, CDROMVOLCTRL, &volctrl ) == 0 );
  }


Msf_time CD::time( const Time_mode mode ) const
  {
  if( timelist.size() ) switch( mode )
    {
    case relative: return time_rel;
    case rem_rel : return time_end( track_ ) - time_start( track_ ) - time_rel;
    case absolute: return time_abs;
    case rem_abs : return timelist.back().end - time_abs;
    }
  return Msf_time();
  }


Msf_time CD::time_start( const int track ) const
  {
  if( timelist.size() && tracks() &&
      track >= first_track_ && track <= last_track_ )
    return timelist[track-first_track_].start;
  return Msf_time();
  }


Msf_time CD::time_end( const int track ) const
  {
  if( timelist.size() && tracks() &&
      track >= first_track_ && track <= last_track_ )
    return timelist[track-first_track_].end;
  return Msf_time();
  }


Msf_time CD::time_track( const int track ) const
  {
  if( timelist.size() && tracks() &&
      track >= first_track_ && track <= last_track_ )
    return timelist[track-first_track_].end - timelist[track-first_track_].start;
  return Msf_time();
  }


Msf_time CD::time_disc() const
  {
  if( timelist.size() )
    return timelist.back().end - timelist.front().start;
  return Msf_time();
  }
