/*****************************************************************************
 *
 *  jobmanager.cpp -- JobManager is an asynchronous job handler.
 *
 *  Copyright (C) 2010 Starbuck <starbuck@labdv.com>
 *  All rights reserved.
 *
 *  This file is part of the JobManager module of the DVDx 4 application.
 *
 *  GNU Lesser General Public License Usage
 *  This file may be used under the terms of the GNU Lesser General
 *  Public License version 2.1 as published by the Free Software
 *  Foundation and appearing in the file LICENSE.LGPL included in the
 *  packaging of this file.  Please review the following information to
 *  ensure the GNU Lesser General Public License version 2.1 requirements
 *  will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
 *
 *  GNU General Public License Usage
 *  Alternatively, this file may be used under the terms of the GNU
 *  General Public License version 3.0 as published by the Free Software
 *  Foundation and appearing in the file LICENSE.GPL included in the
 *  packaging of this file.  Please review the following information to
 *  ensure the GNU General Public License version 3.0 requirements will be
 *  met: http://www.gnu.org/copyleft/gpl.html.
 *
 *****************************************************************************/

#include "jobmanager.h"
#include "macros.h"
#include "../main/application.h"
#include "../main/paths.h"
#include "../video/video_constants.h"
#include <QProcess>
#include <QSignalMapper>
#include <QFileInfo>
#include <QDateTime>
#include <QDir>
#include <QDebug>
#include <QCoreApplication>
#include <QLibrary>

JobManager::JobManager(QTextEdit *stdIn, QTextEdit *stdOut, QTextEdit *stdErr, QTextEdit *defLog, 
	const int verbosityLevel, const int latencyStart, const int latencyFinished, const int latencyKill)
{
	LatencyStart = (latencyStart == 0) ? DefaultLatencyStart : latencyStart;
	LatencyFinished = (latencyFinished == 0) ? DefaultLatencyFinished : latencyFinished;
	LatencyKill = (latencyKill == 0) ? DefaultLatencyKill : latencyKill;

	// Set IO channels from parent channels
	standardInput = stdIn;
	standardOutput = stdOut;
	standardError = stdErr;
	log = defLog;
	verbosity = verbosityLevel;
}

void JobManager::setLatency(const int latencyStart, const int latencyFinished, const int latencyKill)
{
	LatencyStart = (latencyStart == 0) ? DefaultLatencyStart : latencyStart;
	LatencyFinished = (latencyFinished == 0) ? DefaultLatencyFinished : latencyFinished;
	LatencyKill = (latencyKill == 0) ? DefaultLatencyKill : latencyKill;
}

void JobManager::setVerbosity(const int verbose)
{
	verbosity = verbose;
}

void JobManager::addJob( Job *job )
{
	// Append job
	jobs.append( job );

	processJobs();
}

Job *JobManager::addJob( QString *cmd, QStringList *args, const QString &output,
	const QString &title, const QString &description, 
	Transcoder *transcoder, const qint64 estimatedOutputFileSize )
{
	int engine;
	
	if (transcoder->interfaceName() == "FFmpeg")
	{
		engine = Video::Encoder::FFmpeg;
	}
	else if (transcoder->interfaceName() == "MEncoder")
	{
		engine = Video::Encoder::MEncoder;
	}

	// Create output folder if necessary
	QFileInfo outputInfo(output);
	QDir dir(outputInfo.dir());

	if ( !dir.exists() && !dir.mkpath(outputInfo.canonicalPath()) )
	{
		printLog( tr("Failed to start job %1.").arg(description), VERBOSITY_QUIET );
		return (Job *)0;
	}

	// Test encoder command
	QString testStdIn, testStdOut, testStdErr;
	
	standardInput->clear();
	standardOutput->clear();
	standardError->clear();

	int limit(0);

	/*Experimental: will be implemented with new global encoder instances (4.0.1.0)
	if ( !encoder->isCmdEncodeValid( engine, cmd, args, &output,
		&testStdIn, &testStdOut, &testStdErr, limit ) )
	{
		printLog( tr("Invalid command for job %1.").arg(description), VERBOSITY_QUIET );		
		return (Job *)0;
	}
	*/

	QList<QVariant> *data = new QList<QVariant>;
	*data << QVariant(engine);
	*data << QVariant(0.0);

	if (engine == Video::Encoder::FFmpeg) *data << QVariant((int)estimatedOutputFileSize);
	else *data << QVariant(0); 
	
	*data << QVariant(0.0);
	*data << QVariant(0);  // run_count
	*data << QVariant(output);
	*data << QVariant("");
	*data << QVariant(false);  // never test mode here
	*data << QVariant();  // slot for layout

	Task *task = new Task;
	task->setCommandLine( data, *cmd, *args, outputInfo.canonicalPath(), description, title); 

	// Now we can safely create and process a new job
	Job *job = new Job;
	job->setTask( task, newJobId(), description, outputInfo.fileName(), 
		standardInput, standardOutput, standardError, log, verbosity);

	// Append job
	jobs.append(job);
	processJobs();
	return job;
}

Job *JobManager::addJob( QString *cmd, QStringList *args, const QString &output,
	const QString &title, const QString &description, 
	Transcoder *transcoder, const qint64 estimatedOutputFileSize, const double mediaDuration )
{
	int engine;
	
	if (transcoder->interfaceName() == "FFmpeg")
	{
		engine = Video::Encoder::FFmpeg;
	}
	else if (transcoder->interfaceName() == "MEncoder")
	{
		engine = Video::Encoder::MEncoder;
	}

	// Create output folder if necessary
	QFileInfo outputInfo(output);
	QDir dir(outputInfo.dir());

	if ( !dir.exists() && !dir.mkpath(outputInfo.canonicalPath()) )
	{
		printLog( tr("Failed to start job %1.").arg(description), VERBOSITY_QUIET );
		return (Job *)0;
	}

	// Test encoder command
	QString testStdIn, testStdOut, testStdErr;

	standardInput->clear();
	standardOutput->clear();
	standardError->clear();

	int limit(0);

	/*Experimental: will be implemented with new global encoder instances (4.0.1.0)
	if ( !encoder->isCmdEncodeValid( engine, cmd, args, &output,
		&testStdIn, &testStdOut, &testStdErr, limit ) )
	{
		printLog( tr("Invalid command for job %1.").arg(description), VERBOSITY_QUIET );		
		return (Job *)0;
	}
	*/

	QList<QVariant> *data = new QList<QVariant>;
	*data << QVariant(engine);
	*data << QVariant(mediaDuration);  // will set total time in console

	if (engine == Video::Encoder::FFmpeg) *data << QVariant((int)estimatedOutputFileSize);
	else *data << QVariant(0); 
	
	*data << QVariant(0.0);
	*data << QVariant(0);  // run_count
	*data << QVariant(output);
	*data << QVariant("");
	*data << QVariant(false);  // never test mode here
	*data << QVariant();  // slot for layout

	Task *task = new Task;
	task->setCommandLine( data, *cmd, *args, outputInfo.canonicalPath(), description, title); 

	// Now we can safely create and process a new job
	Job *job = new Job;
	job->setTask( task, newJobId(), description, outputInfo.fileName(), 
		standardInput, standardOutput, standardError, log, verbosity);

	// Append job
	jobs.append(job);
	processJobs();
	return job;
}

QString JobManager::newJobId()
{
	QDateTime stamp( QDateTime::currentDateTime() );
	
	return stamp.toString("yyyyMMdd-hhmmss-zzz");
}

void JobManager::setProcessDone(Process *process)
{
	Task *t = process->getTask();
	if ( t->getState() == Task::Terminated ) t->setState( Task::Done );
}

void JobManager::processJobs()
{
	qDebug("JobManager: processing.");

	if ( jobs.isEmpty() ) return;

	QListIterator<Job *> j(jobs);

	while (j.hasNext())
	{
		Job *job = j.next();

		Task *taskToStart = job->getTaskToStart();

		if (taskToStart == 0) continue;

		taskToStart->setState( Task::Submitting );  // avoid being submitted elsewhere (multi-instances, multi-threading paranoia)

		QString cmd = taskToStart->getCommand();
		QStringList args = taskToStart->getArguments();
		QString workingDir = taskToStart->getDirectory();
		QString taskName = taskToStart->getName();
		Job *ref = (Job *)taskToStart->getJobRef();  // might be job here (from j.next())
		QString jobId = taskToStart->getJobId();
		QString jobName = taskToStart->getJobName();
		
		QFileInfo fi (cmd);
		QString cmdBaseName = fi.completeBaseName() + "." + fi.suffix();

		// Start task!
		QString strIn(cmd);
		strIn.append(" ").append(args.join(" "));
		printIn(strIn);

		qDebug() << "JobManager:"<< jobId << "start" << cmd << args;

		Process *process = new Process();

#ifdef Q_OS_MAC

#elif defined(Q_OS_LINUX)
		// Linux version got its own FFmpeg build
		if (cmd.contains("ffmpeg", Qt::CaseInsensitive))
		{
			// ffmpeg libs are in binary/../lib
			QDir dir( cmd );
			dir.cdUp();
			dir.cdUp();
			dir.cd( "lib" );
			QString ffmpegLibPath = dir.absolutePath();
			QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
			env.insert("LD_LIBRARY_PATH", ffmpegLibPath);
			process->setProcessEnvironment(env);
		}
#endif

		process->setWorkingDirectory(workingDir);
		process->start(cmd, args);

		if (!process->waitForStarted( LatencyStart ))
		{
			taskToStart->setState( Task::StartFailure );  // failed to start
			process->close();
			delete(process);

			qDebug() << "JobManager:" << jobId << "failed to start" << cmd << args;
		
			printLog( QObject::tr("Failed to start %1 [%2].")
				.arg(taskName).arg(jobId), VERBOSITY_QUIET );
		}
		else
		{
			process->setJob( job );
			process->setTask( taskToStart );
			process->setCommand( &cmdBaseName );
			
			processes.append(process);

			Q_PID pid = process->pid();
			taskToStart->setPid( pid );  // task is now running with that PID
			taskToStart->setProcess( (void *)process );

			taskToStart->setState( Task::Running );

			connect ( taskToStart, SIGNAL(killProcess()), process, SLOT(kill()) );
			//*experimental connect ( taskToStart, SIGNAL(stopProcess()), process, SLOT(interrupt()) );

			connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), 
				this, SLOT(handleProcessFinished(int, QProcess::ExitStatus)));

			connect(process, SIGNAL(readyReadStandardOutput()), 
				this, SLOT(readProcessStdOutput()));
			connect(process, SIGNAL(readyReadStandardError()), 
				this, SLOT(readProcessStdError()));

			printLog( QObject::tr("Started job: %1 [%2].")
				.arg(taskName).arg(jobId), VERBOSITY_QUIET );

			Application::setKeepAwake();

			emit processStarted( taskToStart );
		}
	}
}

void JobManager::abortProcesses()
{
	int n = processes.count();

	for (int i = 0; i < n; i++)
	{
		processes.takeFirst()->close();
	}

	processes.clear();

	QCoreApplication::processEvents();  // we want to process all to ensure deleting output files
}

bool JobManager::isIdle()
{
	bool idle;
	
	if ( jobs.isEmpty() )
	{
		idle = true;
	}
	else
	{
		idle = true;
		QListIterator<Job *> j(jobs);
		while ( j.hasNext() )
		{
			idle &= j.next()->isFinished();
		}
	}

	return idle;
}

void JobManager::handleProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
	Process *process = (Process*)sender();

	qDebug() << "JobManager: exit code" << process->getJob()->getName() << process->exitCode();

	bool terminated = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
	process->getJob()->updateOnTerminatedTask( process->getTask(), terminated );

	Application::restoreFromKeepAwake();

	emit processFinished( process, exitCode, exitStatus );
}

void JobManager::readProcessStdOutput()
{
	emit processStdOutReady( (Process*)sender() );
}

void JobManager::readProcessStdError()
{
	emit processStdErrReady( (Process*)sender() );
}

void JobManager::printLog(const QString &message, const int verbosityLevel)
{
	if ( log == 0 )
		qDebug() << "JobManager Log:" << message;
	else if ( verbosityLevel <= verbosity )
	{
		log->append( message );
		QCoreApplication::processEvents();  // we want to show message immediately
	}
}

void JobManager::printIn(const QString &message, const int verbosityLevel)
{
	if ( standardInput == 0)
		qDebug() << "JobManager stdIn:" << message;
	else if ( verbosityLevel <= verbosity )
	{
		standardInput->append( message );
		QCoreApplication::processEvents();  // we want to show message immediately
	}
}

void JobManager::printOut(const QString &message, const int verbosityLevel)
{
	if ( standardOutput == 0)
		qDebug() << "JobManager stdOut:" << message;
	else if ( verbosityLevel <= verbosity )
	{
		standardOutput->append( message );
		QCoreApplication::processEvents();  // we want to show message immediately
	}
}

void JobManager::printErr(const QString &message, const int verbosityLevel)
{
	if ( standardError == 0)
		qDebug() << "JobManager stdErr:" << message;
	else if ( verbosityLevel <= verbosity )
		standardError->append( message );
}

void JobManager::clearLog()
{
	if ( log != 0)
		log->clear();
}

void JobManager::clearIn()
{
	if ( standardInput != 0)
		standardInput->clear();
}

void JobManager::clearOut()
{
	if ( standardOutput != 0)
		standardOutput->clear();
}

void JobManager::clearErr()
{
	if ( standardError != 0)
		standardError->clear();
}
