/*
 *
 * $Id: k3bgrowisofswriter.cpp 731898 2007-11-02 08:22:18Z 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 "k3bgrowisofswriter.h"

#include <k3bcore.h>
#include <k3bdevice.h>
#include <k3bdevicehandler.h>
#include <k3bprocess.h>
#include <k3bexternalbinmanager.h>
#include <k3bversion.h>
#include <k3bdiskinfo.h>
#include <k3bglobals.h>
#include <k3bthroughputestimator.h>
#include "k3bgrowisofshandler.h"
#include <k3bpipebuffer.h>
#include <k3bglobalsettings.h>
#include <k3bdeviceglobals.h>

#include <tdelocale.h>
#include <kdebug.h>
#include <tdeglobal.h>

#include <tqvaluelist.h>
#include <tqfile.h>

#include <unistd.h>


class K3bGrowisofsWriter::Private
{
public:
  Private()
    : writingMode( 0 ),
      closeDvd(false),
      multiSession(false),
      process( 0 ),
      growisofsBin( 0 ),
      trackSize(-1),
      layerBreak(0),
      usingRingBuffer(false),
      ringBuffer(0),
    forceNoEject( false ) {
  }

  int writingMode;
  bool closeDvd;
  bool multiSession;
  K3bProcess* process;
  const K3bExternalBin* growisofsBin;
  TQString image;

  bool success;
  bool canceled;
  bool finished;

  TQTime lastSpeedCalculationTime;
  int lastSpeedCalculationBytes;
  int lastProgress;
  unsigned int lastProgressed;
  double lastWritingSpeed;

  bool writingStarted;

  K3bThroughputEstimator* speedEst;
  K3bGrowisofsHandler* gh;

  // used in DAO with growisofs >= 5.15
  long trackSize;

  long layerBreak;

  unsigned long long overallSizeFromOutput;
  long long firstSizeFromOutput;

  TQFile inputFile;

  bool usingRingBuffer;
  K3bPipeBuffer* ringBuffer;

  TQString multiSessionInfo;

    bool forceNoEject;
};


K3bGrowisofsWriter::K3bGrowisofsWriter( K3bDevice::Device* dev, K3bJobHandler* hdl,
					TQObject* parent, const char* name )
  : K3bAbstractWriter( dev, hdl, parent, name )
{
  d = new Private;
  d->speedEst = new K3bThroughputEstimator( this );
  connect( d->speedEst, TQ_SIGNAL(throughput(int)),
	   this, TQ_SLOT(slotThroughput(int)) );

  d->gh = new K3bGrowisofsHandler( this );
  connect( d->gh, TQ_SIGNAL(infoMessage(const TQString&, int)),
	   this,TQ_SIGNAL(infoMessage(const TQString&, int)) );
  connect( d->gh, TQ_SIGNAL(newSubTask(const TQString&)),
	   this, TQ_SIGNAL(newSubTask(const TQString&)) );
  connect( d->gh, TQ_SIGNAL(buffer(int)),
	   this, TQ_SIGNAL(buffer(int)) );
  connect( d->gh, TQ_SIGNAL(deviceBuffer(int)),
	   this, TQ_SIGNAL(deviceBuffer(int)) );
  connect( d->gh, TQ_SIGNAL(flushingCache()),
	   this, TQ_SLOT(slotFlushingCache()) );
}


K3bGrowisofsWriter::~K3bGrowisofsWriter()
{
  delete d->process;
  delete d;
}


bool K3bGrowisofsWriter::active() const
{
  return (d->process ? d->process->isRunning() : false);
}


int K3bGrowisofsWriter::fd() const
{
  if( d->process ) {
    if( d->usingRingBuffer )
      return d->ringBuffer->inFd();
    else
      return d->process->stdinFd();
  }
  else
    return -1;
}


bool K3bGrowisofsWriter::closeFd()
{
  return ( !::close( fd() ) );
}


bool K3bGrowisofsWriter::prepareProcess()
{
  d->growisofsBin = k3bcore->externalBinManager()->binObject( "growisofs" );
  if( !d->growisofsBin ) {
    emit infoMessage( i18n("Could not find %1 executable.").arg("growisofs"), ERROR );
    return false;
  }

  if( d->growisofsBin->version < K3bVersion( 5, 10 ) ) {
    emit infoMessage( i18n("Growisofs version %1 is too old. "
			   "K3b needs at least version 5.10.").arg(d->growisofsBin->version),
		      ERROR );
    return false;
  }

  emit debuggingOutput( "Used versions", "growisofs: " + d->growisofsBin->version );

  if( !d->growisofsBin->copyright.isEmpty() )
    emit infoMessage( i18n("Using %1 %2 - Copyright (C) %3").arg("growisofs")
		      .arg(d->growisofsBin->version).arg(d->growisofsBin->copyright), INFO );


  //
  // The growisofs bin is ready. Now we add the parameters
  //
  delete d->process;
  d->process = new K3bProcess();
  d->process->setRunPrivileged(true);
  //  d->process->setPriority( TDEProcess::PrioHighest );
  d->process->setSplitStdout(true);
  d->process->setRawStdin(true);
  connect( d->process, TQ_SIGNAL(stderrLine(const TQString&)), this, TQ_SLOT(slotReceivedStderr(const TQString&)) );
  connect( d->process, TQ_SIGNAL(stdoutLine(const TQString&)), this, TQ_SLOT(slotReceivedStderr(const TQString&)) );
  connect( d->process, TQ_SIGNAL(processExited(TDEProcess*)), this, TQ_SLOT(slotProcessExited(TDEProcess*)) );


  //
  // growisofs < 5.20 wants the tracksize to be a multiple of 16 (1 ECC block: 16*2048 bytes)
  // we simply pad ourselves.
  //
  // But since the writer itself properly pads or writes a longer lead-out we don't really need
  // to write zeros. We just tell growisofs to reserve a multiple of 16 blocks.
  // This is only releveant in DAO mode anyway.
  //
  // FIXME: seems as we also need this for double layer writing. Better make it the default and
  //        actually write the pad bytes. The only possibility I see right now is to add a padding option
  //        to the pipebuffer.
  int trackSizePadding = 0;
  if( d->trackSize > 0 && d->growisofsBin->version < K3bVersion( 5, 20 ) ) {
    if( d->trackSize % 16 ) {
      trackSizePadding = (16 - d->trackSize%16);
      kdDebug() << "(K3bGrowisofsWriter) need to pad " << trackSizePadding << " blocks." << endl;
    }
  }


  *d->process << d->growisofsBin;

  // set this var to true to enable the ringbuffer
  d->usingRingBuffer = ( d->growisofsBin->version < K3bVersion( 6, 0 ) );

  TQString s = burnDevice()->blockDeviceName() + "=";
  if( d->usingRingBuffer || d->image.isEmpty() ) {
    // we always read from stdin since the ringbuffer does the actual reading from the source
    s += "/dev/fd/0";
  }
  else
    s += d->image;

  if( d->multiSession && !d->multiSessionInfo.isEmpty() )
    *d->process << "-C" << d->multiSessionInfo;

  if( d->multiSession )
    *d->process << "-M";
  else
    *d->process << "-Z";
  *d->process << s;


  if( !d->image.isEmpty() && d->usingRingBuffer ) {
    d->inputFile.setName( d->image );
    d->trackSize = (K3b::filesize( d->image ) + 1024) / 2048;
    if( !d->inputFile.open( IO_ReadOnly ) ) {
      emit infoMessage( i18n("Could not open file %1.").arg(d->image), ERROR );
      return false;
    }
  }

  // now we use the force (luke ;) do not reload the dvd, K3b does that.
  *d->process << "-use-the-force-luke=notray";

  // we check for existing filesystems ourselves, so we always force the overwrite...
  *d->process << "-use-the-force-luke=tty";

  bool dvdCompat = d->closeDvd;

  // DL writing with forced layer break
  if( d->layerBreak > 0 ) {
    *d->process << "-use-the-force-luke=break:" + TQString::number(d->layerBreak);
    dvdCompat = true;
  }

  // the tracksize parameter takes priority over the dao:tracksize parameter since growisofs 5.18
  else if( d->growisofsBin->version > K3bVersion( 5, 17 ) && d->trackSize > 0 )
    *d->process << "-use-the-force-luke=tracksize:" + TQString::number(d->trackSize + trackSizePadding);

  if( simulate() )
    *d->process << "-use-the-force-luke=dummy";

  if( d->writingMode == K3b::DAO ) {
    dvdCompat = true;
    if( d->growisofsBin->version >= K3bVersion( 5, 15 ) && d->trackSize > 0 )
      *d->process << "-use-the-force-luke=dao:" + TQString::number(d->trackSize + trackSizePadding);
    else
      *d->process << "-use-the-force-luke=dao";
    d->gh->reset( burnDevice(), true );
  }
  else
    d->gh->reset( burnDevice(), false );

  //
  // Never use the -dvd-compat parameter with DVD+RW media
  // because the only thing it does is creating problems.
  // Normally this should be done in growisofs
  //
  int mediaType = burnDevice()->mediaType();
  if( dvdCompat &&
      mediaType != K3bDevice::MEDIA_DVD_PLUS_RW &&
      mediaType != K3bDevice::MEDIA_DVD_RW_OVWR )
    *d->process << "-dvd-compat";

  //
  // Some DVD writers do not allow changing the writing speed so we allow
  // the user to ignore the speed setting
  //
  int speed = burnSpeed();
  if( speed >= 0 ) {
    if( speed == 0 ) {
      // try to determine the writeSpeed
      // if it fails determineOptimalWriteSpeed() will return 0 and
      // the choice is left to growisofs which means that the choice is
      // really left to the drive since growisofs does not change the speed
      // if no option is given
      speed = burnDevice()->determineMaximalWriteSpeed();
    }

    // speed may be a float number. example: DVD+R(W): 2.4x
    if( speed != 0 )
      *d->process << TQString("-speed=%1").arg( speed%1385 > 0
					      ? TQString::number( (float)speed/1385.0, 'f', 1 )
					      : TQString::number( speed/1385 ) );
  }

  if( k3bcore->globalSettings()->overburn() )
    *d->process << "-overburn";

  if( !d->usingRingBuffer && d->growisofsBin->version >= K3bVersion( 6, 0 ) ) {
    bool manualBufferSize = k3bcore->globalSettings()->useManualBufferSize();
    int bufSize = ( manualBufferSize ? k3bcore->globalSettings()->bufferSize() : 32 );
    *d->process << TQString("-use-the-force-luke=bufsize:%1m").arg(bufSize);
  }

  // additional user parameters from config
  const TQStringList& params = d->growisofsBin->userParameters();
  for( TQStringList::const_iterator it = params.begin(); it != params.end(); ++it )
    *d->process << *it;

  emit debuggingOutput( "Burned media", K3bDevice::mediaTypeString(mediaType) );

  return true;
}


void K3bGrowisofsWriter::start()
{
  jobStarted();

  d->lastWritingSpeed = 0;
  d->lastProgressed = 0;
  d->lastProgress = 0;
  d->firstSizeFromOutput = -1;
  d->lastSpeedCalculationTime = TQTime::currentTime();
  d->lastSpeedCalculationBytes = 0;
  d->writingStarted = false;
  d->canceled = false;
  d->speedEst->reset();
  d->finished = false;

  if( !prepareProcess() ) {
    jobFinished( false );
  }
  else {

    kdDebug() << "***** " << d->growisofsBin->name() << " parameters:\n";
    const TQValueList<TQCString>& args = d->process->args();
    TQString s;
    for( TQValueList<TQCString>::const_iterator it = args.begin(); it != args.end(); ++it ) {
      s += *it + " ";
    }
    kdDebug() << s << flush << endl;
    emit debuggingOutput( d->growisofsBin->name() + " command:", s);


    emit newSubTask( i18n("Preparing write process...") );

    // FIXME: check the return value
    if( K3b::isMounted( burnDevice() ) ) {
      emit infoMessage( i18n("Unmounting medium"), INFO );
      K3b::unmount( burnDevice() );
    }

    // block the device (including certain checks)
    k3bcore->blockDevice( burnDevice() );

    // lock the device for good in this process since it will
    // be opened in the growisofs process
    burnDevice()->close();
    burnDevice()->usageLock();

    if( !d->process->start( TDEProcess::NotifyOnExit, TDEProcess::All ) ) {
      // something went wrong when starting the program
      // it "should" be the executable
      kdDebug() << "(K3bGrowisofsWriter) could not start " << d->growisofsBin->path << endl;
      emit infoMessage( i18n("Could not start %1.").arg(d->growisofsBin->name()), K3bJob::ERROR );
      jobFinished(false);
    }
    else {
      if( simulate() ) {
	emit newTask( i18n("Simulating") );
	emit infoMessage( i18n("Starting simulation..."),
			  K3bJob::INFO );
      }
      else {
	emit newTask( i18n("Writing") );
	emit infoMessage( i18n("Starting disc write..."), K3bJob::INFO );
      }

      d->gh->handleStart();

      // create the ring buffer
      if( d->usingRingBuffer ) {
	if( !d->ringBuffer ) {
	  d->ringBuffer = new K3bPipeBuffer( this, this );
	  connect( d->ringBuffer, TQ_SIGNAL(percent(int)), this, TQ_SIGNAL(buffer(int)) );
	  connect( d->ringBuffer, TQ_SIGNAL(finished(bool)), this, TQ_SLOT(slotRingBufferFinished(bool)) );
	}

	d->ringBuffer->writeToFd( d->process->stdinFd() );
	bool manualBufferSize = k3bcore->globalSettings()->useManualBufferSize();
	int bufSize = ( manualBufferSize ? k3bcore->globalSettings()->bufferSize() : 20 );
	d->ringBuffer->setBufferSize( bufSize );

	if( !d->image.isEmpty() )
	  d->ringBuffer->readFromFd( d->inputFile.handle() );

	d->ringBuffer->start();
      }
    }
  }
}


void K3bGrowisofsWriter::cancel()
{
  if( active() ) {
    d->canceled = true;
    closeFd();
    if( d->usingRingBuffer && d->ringBuffer )
      d->ringBuffer->cancel();
    d->process->kill();
  }
}


void K3bGrowisofsWriter::setWritingMode( int m )
{
  d->writingMode = m;
}


void K3bGrowisofsWriter::setTrackSize( long size )
{
  d->trackSize = size;
}


void K3bGrowisofsWriter::setLayerBreak( long lb )
{
  d->layerBreak = lb;
}


void K3bGrowisofsWriter::setCloseDvd( bool b )
{
  d->closeDvd = b;
}


void K3bGrowisofsWriter::setMultiSession( bool b )
{
  d->multiSession = b;
}


void K3bGrowisofsWriter::setImageToWrite( const TQString& filename )
{
  d->image = filename;
}


void K3bGrowisofsWriter::slotReceivedStderr( const TQString& line )
{
  emit debuggingOutput( d->growisofsBin->name(), line );

  if( line.contains( "remaining" ) ) {

    if( !d->writingStarted ) {
      d->writingStarted = true;
      emit newSubTask( i18n("Writing data") );
    }

    // parse progress
    int pos = line.find( "/" );
    unsigned long long done = line.left( pos ).toULongLong();
    bool ok = true;
    d->overallSizeFromOutput = line.mid( pos+1, line.find( "(", pos ) - pos - 1 ).toULongLong( &ok );
    if( d->firstSizeFromOutput == -1 )
      d->firstSizeFromOutput = done;
    done -= d->firstSizeFromOutput;
    d->overallSizeFromOutput -= d->firstSizeFromOutput;
    if( ok ) {
      int p = (int)(100 * done / d->overallSizeFromOutput);
      if( p > d->lastProgress ) {
	emit percent( p );
	emit subPercent( p );
	d->lastProgress = p;
      }
      if( (unsigned int)(done/1024/1024) > d->lastProgressed ) {
	d->lastProgressed = (unsigned int)(done/1024/1024);
	emit processedSize( d->lastProgressed, (int)(d->overallSizeFromOutput/1024/1024)  );
	emit processedSubSize( d->lastProgressed, (int)(d->overallSizeFromOutput/1024/1024)  );
      }

      // try parsing write speed (since growisofs 5.11)
      pos = line.find( '@' );
      if( pos != -1 ) {
	pos += 1;
	double speed = line.mid( pos, line.find( 'x', pos ) - pos ).toDouble(&ok);
	if( ok ) {
	  if( d->lastWritingSpeed != speed )
	    emit writeSpeed( (int)(speed*1385.0), 1385 );
	  d->lastWritingSpeed = speed;
	}
	else
	  kdDebug() << "(K3bGrowisofsWriter) speed parsing failed: '"
		    << line.mid( pos, line.find( 'x', pos ) - pos ) << "'" << endl;
      }
      else {
	d->speedEst->dataWritten( done/1024 );
      }
    }
    else
      kdDebug() << "(K3bGrowisofsWriter) progress parsing failed: '"
		<< line.mid( pos+1, line.find( "(", pos ) - pos - 1 ).stripWhiteSpace() << "'" << endl;
  }

  //  else
  // to be able to parse the ring buffer fill in growisofs 6.0 we need to do this all the time
  // FIXME: get rid of the K3bGrowisofsHandler once it is sure that we do not need the K3bGrowisofsImager anymore
    d->gh->handleLine( line );
}


void K3bGrowisofsWriter::slotProcessExited( TDEProcess* p )
{
  d->inputFile.close();

  // release the device within this process
  burnDevice()->usageUnlock();

  // unblock the device
  k3bcore->unblockDevice( burnDevice() );

  if( d->canceled ) {
    if( !d->finished ) {
      d->finished = true;
      // this will unblock and eject the drive and emit the finished/canceled signals
      K3bAbstractWriter::cancel();
    }
    return;
  }

  d->finished = true;

  // it seems that growisofs sometimes exits with a valid exit code while a write error occured
  if( p->exitStatus() == 0 && d->gh->error() != K3bGrowisofsHandler::ERROR_WRITE_FAILED ) {

    int s = d->speedEst->average();
    if( s > 0 )
      emit infoMessage( i18n("Average overall write speed: %1 KB/s (%2x)")
                        .arg(s).arg(TDEGlobal::locale()->formatNumber((double)s/1385.0), 2), INFO );

    if( simulate() )
      emit infoMessage( i18n("Simulation successfully completed"), K3bJob::SUCCESS );
    else
      emit infoMessage( i18n("Writing successfully completed"), K3bJob::SUCCESS );

    d->success = true;
  }
  else {
    if( !wasSourceUnreadable() )
      d->gh->handleExit( p->exitStatus() );
    d->success = false;
  }

  if( !k3bcore->globalSettings()->ejectMedia() || d->forceNoEject )
    jobFinished(d->success);
  else {
    emit newSubTask( i18n("Ejecting DVD") );
    connect( K3bDevice::eject( burnDevice() ),
	     TQ_SIGNAL(finished(K3bDevice::DeviceHandler*)),
	     this,
	     TQ_SLOT(slotEjectingFinished(K3bDevice::DeviceHandler*)) );
  }
}


void K3bGrowisofsWriter::slotRingBufferFinished( bool )
{
  if( !d->finished ) {
    d->finished = true;
    // this will unblock and eject the drive and emit the finished/canceled signals
    K3bAbstractWriter::cancel();
  }
}


void K3bGrowisofsWriter::slotEjectingFinished( K3bDevice::DeviceHandler* dh )
{
  if( !dh->success() )
    emit infoMessage( i18n("Unable to eject media."), ERROR );

  jobFinished(d->success);
}


void K3bGrowisofsWriter::slotThroughput( int t )
{
  emit writeSpeed( t, 1385 );
}


void K3bGrowisofsWriter::slotFlushingCache()
{
  if( !d->canceled ) {
    //
    // growisofs's progress output stops before 100%, so we do it manually
    //
    emit percent( 100 );
    emit processedSize( d->overallSizeFromOutput/1024/1024,
			d->overallSizeFromOutput/1024/1024 );
  }
}


void K3bGrowisofsWriter::setMultiSessionInfo( const TQString& info )
{
  d->multiSessionInfo = info;
}


void K3bGrowisofsWriter::setForceNoEject( bool b )
{
    d->forceNoEject = b;
}

#include "k3bgrowisofswriter.moc"
