/* 
 *
 * $Id: k3bcuefileparser.cpp 619556 2007-01-03 17:38:12Z trueg $
 * Copyright (C) 2003 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 "k3bcuefileparser.h"

#include <k3bmsf.h>
#include <k3bglobals.h>
#include <k3btrack.h>
#include <k3bcdtext.h>

#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqtextstream.h>
#include <tqregexp.h>
#include <tqdir.h>

#include <kdebug.h>


// avoid usage of TQTextStream since K3b often
// tries to open big files (iso images) in a 
// cue file parser to test it.
static TQString readLine( TQFile* f )
{
  TQString s;
  TQ_LONG r = f->readLine( s, 1024 );
  if( r >= 0 ) {
    // remove the trailing newline
    return s.stripWhiteSpace();
  }
  else {
    // end of file or error
    return TQString();
  }
}


// TODO: add method: usableByCdrecordDirectly()
// TODO: add Toc with sector sizes

class K3bCueFileParser::Private
{
public:
  bool inFile;
  bool inTrack;
  int trackType;
  int trackMode;
  bool rawData;
  bool haveIndex1;
  K3b::Msf currentDataPos;
  K3b::Msf index0;

  K3bDevice::Toc toc;
  int currentParsedTrack;

  K3bDevice::CdText cdText;
};



K3bCueFileParser::K3bCueFileParser( const TQString& filename )
  : K3bImageFileReader()
{
  d = new Private;
  openFile( filename );
}


K3bCueFileParser::~K3bCueFileParser()
{
  delete d;
}


void K3bCueFileParser::readFile()
{
  setValid(true);

  d->inFile = d->inTrack = d->haveIndex1 = false;
  d->trackMode = K3bDevice::Track::UNKNOWN;
  d->toc.clear();
  d->cdText.clear();
  d->currentParsedTrack = 0;

  TQFile f( filename() );
  if( f.open( IO_ReadOnly ) ) {
    TQString line = readLine( &f );
    while( !line.isNull() ) {
      
      if( !parseLine(line) ) {
	setValid(false);
	break;
      }

      line = readLine( &f );
    }

    if( isValid() ) {
      // save last parsed track for which we do not have the proper length :(
      if( d->currentParsedTrack > 0 ) {
	d->toc.append( K3bDevice::Track( d->currentDataPos, 
					   d->currentDataPos,
					   d->trackType,
					   d->trackMode ) );
      }
      
      // debug the toc
      kdDebug() << "(K3bCueFileParser) successfully parsed cue file." << endl
		<< "------------------------------------------------" << endl;
      for( unsigned int i = 0; i < d->toc.count(); ++i ) {
	K3bDevice::Track& track = d->toc[i];
	kdDebug() << "Track " << (i+1) 
		  << " (" << ( track.type() == K3bDevice::Track::AUDIO ? "audio" : "data" ) << ") "
		  << track.firstSector().toString() << " - " << track.lastSector().toString() << endl;
      }
      
      kdDebug() << "------------------------------------------------" << endl;
    }
  }
  else {
    kdDebug() << "(K3bCueFileParser) could not open file " << filename() << endl;
    setValid(false);
  }
}


bool K3bCueFileParser::parseLine( TQString& line )
{
  // use cap(1) for the filename
  static TQRegExp fileRx( "FILE\\s\"?([^\"]*)\"?\\s[^\"\\s]*" );

  // use cap(1) for the flags
  static TQRegExp flagsRx( "FLAGS(\\s(DCP|4CH|PRE|SCMS)){1,4}" );

  // use cap(1) for the tracknumber and cap(2) for the datatype
  static TQRegExp trackRx( "TRACK\\s(\\d{1,2})\\s(AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI/2352)" );

  // use cap(1) for the index number, cap(3) for the minutes, cap(4) for the seconds, cap(5) for the frames,
  // and cap(2) for the MSF value string
  static TQRegExp indexRx( "INDEX\\s(\\d{1,2})\\s((\\d+):([0-5]\\d):((?:[0-6]\\d)|(?:7[0-4])))" );

  // use cap(1) for the MCN
  static TQRegExp catalogRx( "CATALOG\\s(\\w{13,13})" );

  // use cap(1) for the ISRC
  static TQRegExp isrcRx( "ISRC\\s(\\w{5,5}\\d{7,7})" );

  static TQString cdTextRxStr = "\"?([^\"]{0,80})\"?";

  // use cap(1) for the string
  static TQRegExp titleRx( "TITLE\\s" + cdTextRxStr );
  static TQRegExp performerRx( "PERFORMER\\s" + cdTextRxStr );
  static TQRegExp songwriterRx( "SONGWRITER\\s" + cdTextRxStr );


  // simplify all white spaces except those in filenames and CD-TEXT
  simplifyWhiteSpace( line );

  // skip comments and empty lines
  if( line.startsWith("REM") || line.startsWith("#") || line.isEmpty() )
    return true;


  //
  // FILE
  //
  if( fileRx.exactMatch( line ) ) {

    setValid( findImageFileName( fileRx.cap(1) ) );
    
    if( d->inFile ) {
      kdDebug() << "(K3bCueFileParser) only one FILE statement allowed." << endl;
      return false;
    }
    d->inFile = true;
    d->inTrack = false;
    d->haveIndex1 = false;
    return true;
  }


  //
  // TRACK
  //
  else if( trackRx.exactMatch( line ) ) {
    if( !d->inFile ) {
      kdDebug() << "(K3bCueFileParser) TRACK statement before FILE." << endl;
      return false;
    }

    // check if we had index1 for the last track
    if( d->inTrack && !d->haveIndex1 ) {
      kdDebug() << "(K3bCueFileParser) TRACK without INDEX 1." << endl;
      return false;
    }

    // save last track
    // TODO: use d->rawData in some way
    if( d->currentParsedTrack > 0 ) {
      d->toc.append( K3bDevice::Track( d->currentDataPos, 
				       d->currentDataPos,
				       d->trackType,
				       d->trackMode ) );
    }

    d->currentParsedTrack++;

    d->cdText.resize( d->currentParsedTrack );

    // parse the tracktype
    if( trackRx.cap(2) == "AUDIO" ) {
      d->trackType = K3bDevice::Track::AUDIO;
      d->trackMode = K3bDevice::Track::UNKNOWN;
    }
    else {
      d->trackType = K3bDevice::Track::DATA;
      if( trackRx.cap(2).startsWith("MODE1") ) {
	d->trackMode = K3bDevice::Track::MODE1;
	d->rawData = (trackRx.cap(2) == "MODE1/2352");
      }
      else if( trackRx.cap(2).startsWith("MODE2") ) {
	d->trackMode = K3bDevice::Track::MODE2;
	d->rawData = (trackRx.cap(2) == "MODE2/2352");
      }
      else {
	kdDebug() << "(K3bCueFileParser) unsupported track type: " << TQString(trackRx.cap(2)) << endl;
	return false;
      }
    }

    d->haveIndex1 = false;
    d->inTrack = true;
    d->index0 = 0;

    return true;
  }


  //
  // FLAGS
  //
  else if( flagsRx.exactMatch( line ) ) {
    if( !d->inTrack ) {
      kdDebug() << "(K3bCueFileParser) FLAGS statement without TRACK." << endl;
      return false;
    }

    // TODO: save the flags
    return true;
  }


  //
  // INDEX
  //
  else if( indexRx.exactMatch( line ) ) {
    if( !d->inTrack ) {
      kdDebug() << "(K3bCueFileParser) INDEX statement without TRACK." << endl;
      return false;
    }

    unsigned int indexNumber = indexRx.cap(1).toInt();

    K3b::Msf indexStart = K3b::Msf::fromString( indexRx.cap(2) );

    if( indexNumber == 0 ) {
      d->index0 = indexStart;

      if( d->currentParsedTrack < 2 && indexStart > 0 ) {
	kdDebug() << "(K3bCueFileParser) first track is not allowed to have a pregap > 0." << endl;
	return false;
      }
    }
    else if( indexNumber == 1 ) {
      d->haveIndex1 = true;
      d->currentDataPos = indexStart;
      if( d->currentParsedTrack > 1 ) {
	d->toc[d->currentParsedTrack-2].setLastSector( indexStart-1 );
	if( d->index0 > 0 && d->index0 < indexStart ) {
	  d->toc[d->currentParsedTrack-2].setIndex0( d->index0 - d->toc[d->currentParsedTrack-2].firstSector() );
	}
      }
    }
    else {
      // TODO: add index > 0
    }

    return true;
  }


  //
  // CATALOG
  //
  if( catalogRx.exactMatch( line ) ) {
    // TODO: set the toc's mcn
    return true;
  }


  //
  // ISRC
  //
  if( isrcRx.exactMatch( line ) ) {
    if( d->inTrack ) {
      // TODO: set the track's ISRC
      return true;
    }
    else {
      kdDebug() << "(K3bCueFileParser) ISRC without TRACK." << endl;
      return false;
    }
  }


  //
  // CD-TEXT
  // TODO: create K3bDevice::TrackCdText entries
  //
  else if( titleRx.exactMatch( line ) ) {
    if( d->inTrack )
      d->cdText[d->currentParsedTrack-1].setTitle( titleRx.cap(1) );
    else
      d->cdText.setTitle( titleRx.cap(1) );
    return true;
  }

  else if( performerRx.exactMatch( line ) ) {
    if( d->inTrack )
      d->cdText[d->currentParsedTrack-1].setPerformer( performerRx.cap(1) );
    else
      d->cdText.setPerformer( performerRx.cap(1) );
    return true;
  }

  else if( songwriterRx.exactMatch( line ) ) {
    if( d->inTrack )
      d->cdText[d->currentParsedTrack-1].setSongwriter( songwriterRx.cap(1) );
    else
      d->cdText.setSongwriter( songwriterRx.cap(1) );
    return true;
  }

  else {
    kdDebug() << "(K3bCueFileParser) unknown Cue line: '" << line << "'" << endl;
    return false;
  }
}


void K3bCueFileParser::simplifyWhiteSpace( TQString& s )
{
  s = s.stripWhiteSpace();

  unsigned int i = 0;
  bool insideQuote = false;
  while( i < s.length() ) {
    if( !insideQuote ) {
      if( s[i].isSpace() && s[i+1].isSpace() )
	s.remove( i, 1 );
    }

    if( s[i] == '"' )
      insideQuote = !insideQuote;

    ++i;
  }
}


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


const K3bDevice::CdText& K3bCueFileParser::cdText() const
{
  return d->cdText;
}


bool K3bCueFileParser::findImageFileName( const TQString& dataFile )
{
  //
  // CDRDAO does not use this image filename but replaces the extension from the cue file
  // with "bin" to get the image filename, we should take this into account
  //

  m_imageFilenameInCue = true;

  // first try filename as a hole (absolut)
  if( TQFile::exists( dataFile ) ) {
    setImageFilename( TQFileInfo(dataFile).absFilePath() );
    return true;
  }

  // try the filename in the cue's directory
  if( TQFileInfo( K3b::parentDir(filename()) + dataFile.section( '/', -1 ) ).isFile() ) {
    setImageFilename( K3b::parentDir(filename()) + dataFile.section( '/', -1 ) );
    kdDebug() << "(K3bCueFileParser) found image file: " << imageFilename() << endl;
    return true;
  }

  // try the filename ignoring case
  if( TQFileInfo( K3b::parentDir(filename()) + TQString(dataFile.section( '/', -1 )).lower() ).isFile() ) {
    setImageFilename( K3b::parentDir(filename()) + TQString(dataFile.section( '/', -1 )).lower() );
    kdDebug() << "(K3bCueFileParser) found image file: " << imageFilename() << endl;
    return true;
  }

  m_imageFilenameInCue = false;

  // try removing the ending from the cue file (image.bin.cue and image.bin)
  if( TQFileInfo( filename().left( filename().length()-4 ) ).isFile() ) {
    setImageFilename( filename().left( filename().length()-4 ) );
    kdDebug() << "(K3bCueFileParser) found image file: " << imageFilename() << endl;
    return true;
  }

  //
  // we did not find the image specified in the cue.
  // Search for another one having the same filename as the cue but a different extension
  //

  TQDir parentDir( K3b::parentDir(filename()) );
  TQString filenamePrefix = filename().section( '/', -1 );
  filenamePrefix.truncate( filenamePrefix.length() - 3 ); // remove cue extension
  kdDebug() << "(K3bCueFileParser) checking folder " << parentDir.path() << " for files: " << filenamePrefix << "*" << endl;

  //
  // we cannot use the nameFilter in TQDir because of the spaces that may occur in filenames
  //
  TQStringList possibleImageFiles = parentDir.entryList( TQDir::Files );
  int cnt = 0;
  for( TQStringList::const_iterator it = possibleImageFiles.constBegin(); it != possibleImageFiles.constEnd(); ++it ) {
    if( (*it).lower() == TQString(dataFile.section( '/', -1 )).lower() ||
	(*it).startsWith( filenamePrefix ) && !(*it).endsWith( "cue" ) ) {
      ++cnt;
      setImageFilename( K3b::parentDir(filename()) + *it );
    }
  }

  //
  // we only do this if there is one unique file which fits the requirements. 
  // Otherwise we cannot be certain to have the right file.
  //
  return ( cnt == 1 && TQFileInfo( imageFilename() ).isFile() );
}
