/****************************************************************************
**
** 		Created using Monkey Studio IDE v1.8.4.0 (1.8.4.0)
** Authors   : Filipe Azevedo aka Nox P@sNox <pasnox@gmail.com>
** Project   : QWBFS Manager
** FileName  : WorkerThread.cpp
** Date      : 2010-06-16T14:19:29
** License   : GPL2
** Home Page : http://code.google.com/p/qwbfs
** Comment   : QWBFS Manager is a cross platform WBFS manager developed using C++/Qt4.
** It's currently working fine under Windows (XP to Seven, 32 & 64Bits), Mac OS X (10.4.x to 10.6.x), Linux & unix like.
**
** 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 3 of the License, or
** (at your option) any later version.
**
** This package 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/>.
**
** In addition, as a special exception, the copyright holders give permission
** to link this program with the OpenSSL project's "OpenSSL" library (or with
** modified versions of it that use the same license as the "OpenSSL"
** library), and distribute the linked executables. You must obey the GNU
** General Public License in all respects for all of the code used other than
** "OpenSSL".  If you modify file(s), you may extend this exception to your
** version of the file(s), but you are not obligated to do so. If you do not
** wish to do so, delete this exception statement from your version.
**
****************************************************************************/
#include "WorkerThread.h"

#include <FreshCore/pCoreUtils>

#include <QTime>
#include <QWidget>
#include <QDir>
#include <QMetaType>
#include <QDebug>

WorkerThread::WorkerThread( QObject* parent )
	: QThread( parent )
{
	qRegisterMetaType<QWBFS::Model::Disc>( "QWBFS::Model::Disc" );
}

WorkerThread::~WorkerThread()
{
	if ( isRunning() ) {
		qWarning() << "Waiting thread to finish...";
		stop();
		wait();
	}
	
	//qWarning() << Q_FUNC_INFO;
}

WorkerThread::Task WorkerThread::task() const
{
	QMutexLocker locker( &const_cast<WorkerThread*>( this )->mMutex );
	return mWork.task;
}

bool WorkerThread::setWork( const WorkerThread::Work& work )
{
	if ( isRunning() ) {
		Q_ASSERT( 0 );
		return false;
	}
	
	mWork = work;
	mWork.window->setWindowTitle( taskToWindowTitle( mWork.task ) );
	
	start();
	
	return true;
}

QString WorkerThread::taskToWindowTitle( WorkerThread::Task task, bool indirect )
{
	return taskToLabel( task, indirect ).append( "..." );
}

QString WorkerThread::taskToLabel( WorkerThread::Task task, bool indirect )
{
	switch ( task ) {
		case WorkerThread::ExportISO:
			return indirect ? tr( "Indirect Export to ISO" ) : tr( "Export to ISO" );
		case WorkerThread::ExportWBFS:
			return indirect ? tr( "Indirect Export to WBFS" ) : tr( "Export to WBFS" );
		case WorkerThread::ImportISO:
			return indirect ? tr( "Indirect Import to ISO" ) : tr( "Import to ISO" );
		case WorkerThread::ImportWBFS:
			return indirect ? tr( "Indirect Import to WBFS" ) : tr( "Import to WBFS" );
		case WorkerThread::ConvertISO:
			return indirect ? tr( "Indirect Convert to ISO" ) : tr( "Convert to ISO" );
		case WorkerThread::ConvertWBFS:
			return indirect ? tr( "Indirect Convert to WBFS" ) : tr( "Convert to WBFS" );
		case WorkerThread::RenameAll:
			return tr( "Rename Disc" );
	}
	
	return QString::null;
}

void WorkerThread::stop()
{
	QMutexLocker locker( &mMutex );
	mStop = true;
	emit canceled();
}

void WorkerThread::run()
{
	WorkerThread::Work work;
	int count = 0;
	int id = 0;
	
	{
		QMutexLocker locker( &mMutex );
		mStop = false;
		work = mWork;
	}
	
	emit globalProgressChanged( id, count );
	
	if ( work.task == WorkerThread::RenameAll ) {
		QDir dir( work.target );
		QStringList filters;
		
		dir.setFilter( QDir::Files );
		
		if ( work.task & WorkerThread::WBFS ) {
			filters << "*.wbfs";
		}
		
		if ( work.task & WorkerThread::ISO ) {
			filters << "*.iso";
		}
		
		foreach ( const QString& filePath, pCoreUtils::findFiles( dir, filters, true ) ) {
			work.discs << QWBFS::Model::Disc( filePath );
		}
	}
	
	count = work.discs.count();
	
	emit globalProgressChanged( id, count );
	
	foreach ( QWBFS::Model::Disc disc, work.discs ) {
		switch ( work.task ) {
			case WorkerThread::RenameAll:
				renameDisc( work.task, disc, work.target, work.pattern, work.invalidChars );
				break;
			case WorkerThread::ExportISO:
			case WorkerThread::ExportWBFS:
			case WorkerThread::ImportISO:
			case WorkerThread::ImportWBFS:
			case WorkerThread::ConvertISO:
			case WorkerThread::ConvertWBFS: {
				switch ( QWBFS::Driver::fileType( disc.origin ) ) {
					case QWBFS::Driver::WBFSFile:
					case QWBFS::Driver::WBFSPartitionFile: {
						switch ( QWBFS::Driver::fileType( work.target ) ) {
							case QWBFS::Driver::WBFSPartitionFile: {
								wbfsToWBFS( work.task, disc, work.target, false, work.invalidChars );
								break;
							}
							case QWBFS::Driver::WBFSFile:
							case QWBFS::Driver::ISOFile:
							case QWBFS::Driver::UnknownFile: {
								if ( work.task & WorkerThread::WBFS ) {
									wbfsToWBFS( work.task, disc, work.target, true, work.invalidChars );
								}
								else if ( work.task & WorkerThread::ISO ) {
									wbfsToISO( work.task, disc, work.target, work.invalidChars );
								}
								else {
									if ( work.task & WorkerThread::Import ) {
										disc.error = QWBFS::Driver::DiscAddFailed;
									}
									else if ( work.task & WorkerThread::Export ) {
										disc.error = QWBFS::Driver::DiscExtractFailed;
									}
									else if ( work.task & WorkerThread::Convert ) {
										disc.error = QWBFS::Driver::DiscConvertFailed;
									}
									else {
										disc.error = QWBFS::Driver::UnknownError;
									}
								}
								
								break;
							}
						}
						
						break;
					}
					case QWBFS::Driver::ISOFile: {
						switch ( QWBFS::Driver::fileType( work.target ) ) {
							case QWBFS::Driver::WBFSPartitionFile: {
								isoToWBFS( work.task, disc, work.target, false, work.invalidChars );
								break;
							}
							case QWBFS::Driver::WBFSFile:
							case QWBFS::Driver::ISOFile:
							case QWBFS::Driver::UnknownFile: {
								if ( work.task & WorkerThread::WBFS ) {
									isoToWBFS( work.task, disc, work.target, true, work.invalidChars );
								}
								else if ( work.task & WorkerThread::ISO ) {
									isoToISO( work.task, disc, work.target, work.invalidChars );
								}
								else {
									if ( work.task & WorkerThread::Import ) {
										disc.error = QWBFS::Driver::DiscAddFailed;
									}
									else if ( work.task & WorkerThread::Export ) {
										disc.error = QWBFS::Driver::DiscExtractFailed;
									}
									else if ( work.task & WorkerThread::Convert ) {
										disc.error = QWBFS::Driver::DiscConvertFailed;
									}
									else {
										disc.error = QWBFS::Driver::UnknownError;
									}
								}
								
								break;
							}
						}
						
						break;
					}
					case QWBFS::Driver::UnknownFile:
						disc.error = QWBFS::Driver::UnknownError;
						break;
				}
			}
		}
		
		disc.state = disc.error == QWBFS::Driver::Ok ? QWBFS::Driver::Success : QWBFS::Driver::Failed;
		
		emit globalProgressChanged( ++id, count );
		emit jobFinished( disc );
		
		{
			QMutexLocker locker( &mMutex );
			
			if ( mStop ) {
				break;
			}
		}
	}
	
	emit globalProgressChanged( count, count );
}

void WorkerThread::connectDriver( QWBFS::Driver* driver )
{
	connect( driver, SIGNAL( currentProgressChanged( int, int, const QTime& ) ), this, SIGNAL( currentProgressChanged( int, int, const QTime& ) ) );
	connect( driver, SIGNAL( globalProgressChanged( int, int ) ), this, SIGNAL( globalProgressChanged( int, int ) ) );
}

void WorkerThread::renameDisc( WorkerThread::Task task, QWBFS::Model::Disc& source, const QString& target, const QString& pattern, const QString& invalidChars )
{
	if ( !source.isValid() ) {
		source.error = QWBFS::Driver::InvalidDisc;
		return;
	}
	
	/*
		%title = Game Title
		%id = Game ID
		%suffix = File Suffix
	*/
	QString filePath = QString( "%1/%2" )
		.arg( target )
		.arg( pattern )
		.replace( "%title", QWBFS::Model::Disc::cleanupGameTitle( source.title, invalidChars ), Qt::CaseInsensitive )
		.replace( "%id", source.id.toUpper(), Qt::CaseInsensitive )
		.replace( "%suffix", QFileInfo( source.origin ).suffix(), Qt::CaseInsensitive )
		;
	
	emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task ) ).arg( source.baseName( invalidChars ) ) );
	emit currentProgressChanged( 0, 1, QTime( 0, 0, 0 ) );
	
	if ( source.origin != filePath ) {
		source.error = QDir( target ).mkpath( QFileInfo( filePath ).absolutePath() ) ? QWBFS::Driver::Ok : QWBFS::Driver::UnknownError;
		
		if ( !source.hasError() ) {
			const bool exists = QFile::exists( filePath );
			
			if ( exists ) {
				const QFileInfo file( filePath );
				const QString path = file.absolutePath();
				const QString fileName = file.fileName();
				filePath = QString( "%1/Existing/%2.%3" )
					.arg( path )
					.arg( fileName )
					.arg( QDateTime::currentDateTime().toString( "yyyy-MM-dd hh'h'mm" ) );
				
				QDir( path ).mkpath( "Existing" );
			}
			
			source.error = QFile::rename( source.origin, filePath ) ? QWBFS::Driver::Ok : QWBFS::Driver::DiscRenameFailed;
			
			if ( exists ) {
				source.error = QWBFS::Driver::DiscFound;
			}
			
			if ( !source.hasError() ) {
				const QString sourcePath = QFileInfo( source.origin ).absolutePath();
				
				if ( pCoreUtils::isEmptyDirectory( sourcePath ) ) {
					QDir( sourcePath ).rmdir( sourcePath );
				}
			}
		}
	}
	else {
		source.error = QWBFS::Driver::Ok;
	}
	
	emit currentProgressChanged( 1, 1, QTime( 0, 0, 0 ) );
}

void WorkerThread::isoToWBFS( WorkerThread::Task task, QWBFS::Model::Disc& source, const QString& _target, bool trimWBFS, const QString& invalidChars )
{
	if ( !source.isValid() ) {
		source.error = QWBFS::Driver::InvalidDisc;
		return;
	}
	
	const QFileInfo file( _target );
	QString target = _target;
	bool created = false;
	QWBFS::Partition::Handle handle;
	
	if ( trimWBFS && file.isDir() ) {
		target = QDir::cleanPath( target.append( QString( "/%1.wbfs" ).arg( source.baseName( invalidChars ) ) ) );
	}
	
	if ( trimWBFS ) {
		
		if ( QFile::exists( target ) ) {
			source.error = QWBFS::Driver::DiscFound;
			return;
		}
		
		emit message( tr( "Initializing WBFS disc '%1'..." ).arg( source.baseName( invalidChars ) ) );
		source.error = QWBFS::Driver::allocateFile( target );
		
		if ( source.hasError() ) {
			QFile::remove( target );
			return;
		}
		
		QWBFS::Partition::Properties properties( target );
		properties.reset = true;
		
		emit message( tr( "Formating WBFS disc '%1'..." ).arg( source.baseName( invalidChars ) ) );
		
		handle = QWBFS::Partition::Handle( properties );
	}
	else {
		handle = QWBFS::Driver::getHandle( target, &created );
	}
	
	if ( !handle.isValid() ) {
		if ( created ) {
			QWBFS::Driver::closeHandle( handle );
		}
		
		if ( trimWBFS ) {
			QFile::remove( target );
		}
		
		source.error = QWBFS::Driver::PartitionNotOpened;
		return;
	}
	
	QWBFS::Driver driver( 0, handle );
	connectDriver( &driver );
	
	emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task ) ).arg( source.baseName( invalidChars ) ) );
	source.error = driver.addDiscImage( source.origin );
	
	if ( source.hasError() ) {
		if ( created ) {
			QWBFS::Driver::closeHandle( handle );
		}
		
		if ( trimWBFS ) {
			QFile::remove( target );
		}
		
		return;
	}
	
	if ( trimWBFS ) {
		source.error = driver.trim();
	}
	
	if ( created ) {
		QWBFS::Driver::closeHandle( handle );
	}
}

void WorkerThread::wbfsToISO( WorkerThread::Task task, QWBFS::Model::Disc& source, const QString& _target, const QString& invalidChars )
{
	if ( !source.isValid() ) {
		source.error = QWBFS::Driver::InvalidDisc;
		return;
	}
	
	QFileInfo file( _target );
	QString target = _target;
	
	if ( file.isDir() ) {
		target = QDir::cleanPath( target.append( QString( "/%1.iso" ).arg( source.baseName( invalidChars ) ) ) );
	}
	
	if ( QFile::exists( target ) ) {
		source.error = QWBFS::Driver::DiscFound;
		return;
	}
	
	file.setFile( target );
	
	bool created = false;
	QWBFS::Partition::Handle handle = QWBFS::Driver::getHandle( source.origin, &created );
	
	// check handle validity
	if ( !handle.isValid() ) {
		if ( created ) {
			QWBFS::Driver::closeHandle( handle );
		}
		
		source.error = QWBFS::Driver::PartitionNotOpened;
		return;
	}
	
	QWBFS::Driver driver( 0, handle );
	connectDriver( &driver );
	
	emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task ) ).arg( source.baseName( invalidChars ) ) );
	source.error = driver.extractDisc( source.id, file.absolutePath(), file.fileName() );
	
	if ( source.hasError() ) {
		QFile::remove( target );
	}
	
	if ( created ) {
		QWBFS::Driver::closeHandle( handle );
	}
}

void WorkerThread::isoToISO( WorkerThread::Task task, QWBFS::Model::Disc& source, const QString& _target, const QString& invalidChars )
{
	if ( !source.isValid() ) {
		source.error = QWBFS::Driver::InvalidDisc;
		return;
	}
	
	const QFileInfo file( _target );
	QString target = _target;
	
	if ( file.isDir() ) {
		target = QDir::cleanPath( target.append( QString( "/%1.iso" ).arg( source.baseName( invalidChars ) ) ) );
	}
	
	if ( QFile::exists( target ) ) {
		source.error = QWBFS::Driver::DiscFound;
		return;
	}
	
	emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task ) ).arg( source.baseName( invalidChars ) ) );
	
	// copying the file chunk by chunk instead of using QFile::copy() to be able to see progression
	QFile in( source.origin );
	QFile out( target );
	
	if ( !in.open( QIODevice::ReadOnly ) ) {
		source.error = QWBFS::Driver::DiscReadFailed;
		return;
	}
	
	if ( !out.open( QIODevice::WriteOnly ) ) {
		source.error = QWBFS::Driver::DiscWriteFailed;
		return;
	}
	
	const uint bufferSize = 1024 *1024 *5; // 5 MB buffer size
	char buffer[ bufferSize ];
	int totalRead = 0;
	
	QTime estimatedTime = QWBFS::Driver::estimatedTimeForTask( totalRead, in.size() );
	emit currentProgressChanged( totalRead, in.size(), estimatedTime );
	
	while ( !in.atEnd() ) {
		const qint64 read = in.read( buffer, bufferSize );
		
		if ( read == -1 ) {
			out.close();
			out.remove();
			
			QFile::remove( target );
			source.error = QWBFS::Driver::DiscReadFailed;
			return;
		}
		
		const qint64 write = out.write( buffer, read );
		
		if ( write == -1 ) {
			out.close();
			out.remove();
			
			QFile::remove( target );
			source.error = QWBFS::Driver::DiscWriteFailed;
			return;
		}
		
		totalRead += read;
		
		estimatedTime = QWBFS::Driver::estimatedTimeForTask( totalRead, in.size() );
		emit currentProgressChanged( totalRead, in.size(), estimatedTime );
	}
	
	in.close();
	out.close();
}

void WorkerThread::wbfsToWBFS( WorkerThread::Task task, QWBFS::Model::Disc& source, const QString& _target, bool trimWBFS, const QString& invalidChars )
{
	if ( !source.isValid() ) {
		source.error = QWBFS::Driver::InvalidDisc;
		return;
	}
	
	const QFileInfo file( _target );
	QString target = _target;
	bool sourceCreated = false;
	QWBFS::Partition::Handle sourceHandle = QWBFS::Driver::getHandle( source.origin, &sourceCreated );
	bool targetCreated = false;
	QWBFS::Partition::Handle targetHandle;
	
	if ( !sourceHandle.isValid() ) {
		if ( sourceCreated ) {
			QWBFS::Driver::closeHandle( sourceHandle );
		}
		
		source.error = QWBFS::Driver::PartitionNotOpened;
		return;
	}
	
	if ( trimWBFS && file.isDir() ) {
		target = QDir::cleanPath( target.append( QString( "/%1.wbfs" ).arg( source.baseName( invalidChars ) ) ) );
	}
	
	if ( trimWBFS ) {
		
		if ( QFile::exists( target ) ) {
			if ( sourceCreated ) {
				QWBFS::Driver::closeHandle( sourceHandle );
			}
			
			source.error = QWBFS::Driver::DiscFound;
			return;
		}
		
		emit message( tr( "Initializing WBFS disc '%1'..." ).arg( source.baseName( invalidChars ) ) );
		source.error = QWBFS::Driver::allocateFile( target );
		
		if ( source.hasError() ) {
			if ( sourceCreated ) {
				QWBFS::Driver::closeHandle( sourceHandle );
			}
			
			QFile::remove( target );
			return;
		}
		
		QWBFS::Partition::Properties properties( target );
		properties.reset = true;
		
		emit message( tr( "Formating WBFS disc '%1'..." ).arg( source.baseName( invalidChars ) ) );
		
		targetHandle = QWBFS::Partition::Handle( properties );
	}
	else {
		targetHandle = QWBFS::Driver::getHandle( target, &targetCreated );
	}
	
	if ( !targetHandle.isValid() ) {
		if ( sourceCreated ) {
			QWBFS::Driver::closeHandle( sourceHandle );
		}
		
		if ( targetCreated ) {
			QWBFS::Driver::closeHandle( targetHandle );
		}
		
		if ( trimWBFS ) {
			QFile::remove( target );
		}
		
		source.error = QWBFS::Driver::PartitionNotOpened;
		return;
	}
	
	QWBFS::Driver targetDriver( 0, targetHandle );
	connectDriver( &targetDriver );
	
	// direct drive2drive
	if ( targetDriver.canDrive2Drive( sourceHandle ) == QWBFS::Driver::Ok ) {
		emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task ) ).arg( source.baseName( invalidChars ) ) );
		
		source.error = targetDriver.addDisc( source.id, sourceHandle );
	}
	// indirect drive2drive
	else {
		emit message( QString( "%1 '%2'..." ).arg( taskToLabel( task, true ) ).arg( source.baseName( invalidChars ) ) );
		
		const QFileInfo tmpFile( QString( "%1/%2.iso" ).arg( QDir::tempPath() ).arg( source.baseName( invalidChars ) ) );
		QWBFS::Driver sourceDriver( 0, sourceHandle );
		connectDriver( &sourceDriver );
		
		if ( targetDriver.hasDisc( source.id ) == QWBFS::Driver::DiscNotFound ) {
			source.error = sourceDriver.extractDisc( source.id, tmpFile.absolutePath(), tmpFile.fileName() );
			
			if ( !source.hasError() ) {
				source.error = targetDriver.addDiscImage( tmpFile.absoluteFilePath() );
			}
			
			QFile::remove( tmpFile.absoluteFilePath() );
		}
		else {
			source.error = QWBFS::Driver::DiscFound;
		}
	}
	
	if ( !source.hasError() && trimWBFS ) {
		source.error = targetDriver.trim();
	}
	
	if ( sourceCreated ) {
		QWBFS::Driver::closeHandle( sourceHandle );
	}
	
	if ( targetCreated ) {
		QWBFS::Driver::closeHandle( targetHandle );
	}
	
	if ( source.hasError() && trimWBFS ) {
		QFile::remove( target );
	}
}
