/*
 * Copyright (C) 2006-2007	Andre Beckedorf
 * 					 		<evilJazz _AT_ katastrophos _DOT_ net>
 *
 * This file is based on eMotion by Michael Swieton <mike@swieton.net>.
 * The few remaining parts in this almost completely rewritten file are
 * Copyright (C) 2005 Atmark <atmarkat _AT_ msn _DOT_ com>
 *                    AGAWA Koji <i _AT_ atty _DOT_ jp>
 *                    Michael Swieton <mike@swieton.net>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <qapplication.h>
#include <qfileinfo.h>
#include <qstring.h>
#include <qfile.h>

#include <cstdio>
#include <cstdlib>

#include <sys/socket.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

#include "mplayer.h"
#include "media.h"
#include "quasar.h"
#include "configuration.h"
#include "debug.h"

static QProcess *checkFeaturesProcess;
static bool mplayerHasGetTimePos;
static bool mplayerHasGetPercentPos;
static bool mplayerHasGetTimeLength;
static bool mplayerCanQueueFiles;

MPlayer::MPlayer()
	: quitState_(QuitNull),
	 process_(NULL),
	 buffer_(""),
	 errorMessage_(""),
	 isPaused_(false),
	 isSendingPTS_(false)
{
	register_handler(SIGTERM, this);
	register_handler(SIGSEGV, this);

	connect(this, SIGNAL(eof()), this, SLOT(cleanUp()));
	connect(this, SIGNAL(quit()), this, SLOT(cleanUp()));
	connect(this, SIGNAL(killed()), this, SLOT(cleanUp()));
	
	connect(&timer_, SIGNAL(timeout()), this, SLOT(pollMPlayer()));
	
	connect(&SystemVolume::singleton(), SIGNAL(levelChanged(int)), this, SLOT(systemVolumeChanged(int)));
}

MPlayer::~MPlayer ()
{
	stop();
	unregister_handler(this);
}

void MPlayer::checkFeatures()
{
	mplayerHasGetTimePos = false;
	mplayerHasGetTimeLength = false;
	mplayerHasGetPercentPos = false;
	mplayerCanQueueFiles = false;
	
	QStringList args;
	args << "-input" << "cmdlist";
	
	args.insert(args.begin(), Quasar::config.mplayerPath);
	checkFeaturesProcess = new QProcess(args);

	checkFeaturesProcess->start();
	
	// timeout timer...
	QTime time;
	time.start();
	bool timedOut = false;
	
	while ((checkFeaturesProcess->isRunning() || checkFeaturesProcess->canReadLineStdout()) && !timedOut)
	{
		timedOut = time.elapsed() > 5000;
		checkFeaturesProcessParseLines();
		usleep(1000); // sleep 1 ms
	}
	
	checkFeaturesProcess->tryTerminate();
	usleep(1000000); // wait 1 sec
	if (checkFeaturesProcess->isRunning())
	{
		checkFeaturesProcess->kill();
		usleep(500000); // wait 500 ms
	}
	
	delete checkFeaturesProcess;
	checkFeaturesProcess = NULL;
}

void MPlayer::checkFeaturesProcessParseLines()
{
	while (checkFeaturesProcess->canReadLineStdout())
	{
		QString line = checkFeaturesProcess->readLineStdout();

		DPRINTF("LINE stdout: %s", (const char*)(line.utf8()));		
		
		if (line.startsWith("get_time_pos"))
		{
			mplayerHasGetTimePos = true;
			qDebug("mplayer supports get_time_pos.");
		}
		else if (line.startsWith("get_time_length"))
		{
			mplayerHasGetTimeLength = true;
			qDebug("mplayer supports get_time_length.");
		}
		else if (line.startsWith("get_percent_pos"))
		{
			mplayerHasGetPercentPos = true;
			qDebug("mplayer supports get_percent_pos.");
		}
		else if (line.startsWith("loadfile") && line.find("[Integer]") != -1)
		{
			mplayerCanQueueFiles = true;
			qDebug("mplayer supports advanced syntax for loadfile, i.e. it can queue files.");
		}
	}
}

QStringList MPlayer::getArgListForMedia(Media *media)
{
	QStringList args;
	
	args << Quasar::config.mplayerPath;
	args << "-slave";
	args << "-quiet";
	
	QString videoFilter = "";
	if (media->isVideo())
	{
		// Video Output
		args << "-vo";
		if (Quasar::config.isOverlayEnabled)
		{
			switch (Quasar::config.machineCategory())
			{
			case Configuration::MACHINE_SLC700:
				args << "w100";
				break;
			case Configuration::MACHINE_SLC3000:
				args << "bvdd";
				break;
			case Configuration::MACHINE_GENERIC:
				args << "fbdev";
				break;
			}
		} else
			args << "fbdev";

		// Full Screen
		if (Quasar::config.isFullScreenEnabled) {
			switch (Quasar::config.machineCategory()) {
			case Configuration::MACHINE_SLC700:
				args << "-fs";
				break;
			case Configuration::MACHINE_SLC3000:
				args << "-vm";
				break;
			case Configuration::MACHINE_GENERIC:
				break;
			}
		}

		// Double Buffering
		if (!Quasar::config.isDoubleBufferingEnabled)
			args << "-nodouble";
			
		// Frame Drop
		switch (Quasar::config.framedropMethod)
		{
		case Configuration::FRAMEDROP_NORMAL:
			args << "-framedrop";
			break;
		case Configuration::FRAMEDROP_HARD:
			args << "-hardframedrop";
			break;
		default:
			break;
		}

		if (useSoftVolume())
			args << "-softvol";
		
		// 映像と音声の同期を鈍感にする。
		// 一時的に負荷が高まる映像の再生が安定する。
		args << "-autosync";
		args << "100";

		// FIXME: more generic handling
		if (media->width() == 352 && media->height() == 240)
		{
			args << "-vf-add";
			args << "crop=320:240";
		}

		if (media->width() > media->height())
		{
			if (media->width() > qApp->desktop()->width() ||
				media->height() > qApp->desktop()->height())
			{
				args << "-vf-add";
				args << "crop=640:480";	// FIXME: use qApp->desktop()
			}
		}

		if (!Quasar::config.isOverlayEnabled)
		{
			if (media->width() > media->height())
			{
				args << "-vf-add";
				args << "rotate=1";
			}
		}
	}
	
	// Cache handling...
	QString cacheSize;
	if (Quasar::config.isCacheEnabled)
	{
		args << "-cache";
		cacheSize = QString::number(Quasar::config.cacheSize);
		args << cacheSize;
		
		if (media->location().isLocalFile())
		{
			// for local files, start playback immediately, ie.
			// don't wait till the cache is filled up.
			// We assume local files are read faster than they are played back.
			args << "-cache-min";
			args << "0";
		}
	}

	// これらのサンプリングレート以外をそのまま再生しようとすると非常に
	// 負荷が高くなるので、44100KHzにリサンプリングする
	if (media->audioRate() != 44100 && media->audioRate() != 48000 && media->audioRate() != 22050)
	{
		args << "-af";
		args << "lavcresample=44100";
	}

	QString fileExtension = media->location().fileName();
	fileExtension = fileExtension.right(fileExtension.length() -1 - fileExtension.findRev('.'));
	DPRINTF("fileExtension: %s", (const char*)fileExtension.latin1());
	QString customArguments = Quasar::config.mplayerArguments(fileExtension);
	if (!customArguments.isEmpty())
	{
		DPRINTF("customArguments: %s", (const char*)customArguments.latin1());	
		QStringList opts = QStringList::split(' ', customArguments);
		for (QStringList::Iterator it = opts.begin(); it != opts.end(); ++it)
			args << (*it);
	}
	
	// 初期状態ではOSDを表示しない
	args << "-osdlevel";
	args << "0";

	// その他の追加オプション
	if (Quasar::config.isAdditionalOptionsEnabled)
	{
		QStringList opts = QStringList::split(' ', Quasar::config.additionalOptions);
		for (QStringList::Iterator it = opts.begin(); it != opts.end(); ++it)
			args << (*it);
	}
	
	return args;
}

bool MPlayer::canRecycleMplayer(Media *media)
{
	QStringList args;
	return canRecycleMplayer(media, args);
}

bool MPlayer::canRecycleMplayer(Media *media, QStringList &args)
{
	args = getArgListForMedia(media);
	
	bool argsDiffer = args.count() != currentProcessArgs_.count();
	if (!argsDiffer)	
		for (int i = 0; i < args.count(); ++i)
			if (args[i] != currentProcessArgs_[i])
			{
				argsDiffer = true;
				DPRINTF("%s != %s", (const char *)(currentProcessArgs_[i].latin1()), (const char *)(args[i].latin1()));
				break;
			}
	
	//return false;
	return isRunning() && !argsDiffer;	
}

bool MPlayer::play(Media *media)
{
	ASSERT(media != 0);

	if (!media->isSupported())
	{
		DPRINTF("%s is Not supported", QString(media->location()).latin1());
		return false;
	}
	
	QStringList args;
	errorMessage_ = QString::null;
	quitState_ = QuitNull;
	
	if (!canRecycleMplayer(media, args))
	{
		if (isRunning())
			stop();

		currentProcessArgs_ = args;
		args << media->location().toString();
		
		process_ = new QProcess(args);
		process_->setCommunication(QProcess::Stdin|QProcess::Stdout|QProcess::Stderr|QProcess::DupStderr);
		connect(process_, SIGNAL(readyReadStdout()), this, SLOT(processReadyReadStdout()));
		connect(process_, SIGNAL(processExited()), this, SLOT(processExited()));
	
		isSendingPTS_ = false;
		
		DPRINTF("Starting mplayer slave...");
		if (!process_->start())
		{
			DPRINTF("Could not execute mplayer");
			return false;
		}
	}
	else
	{
		DPRINTF("Recycling mplayer slave");
		QString command = "loadfile \"" + media->location().toString() + "\"";
		sendCommand(command);
	}

	sendCurrentVolume();
	
	if (Quasar::config.isBackendLoggingEnabled)
	{
		logStream_.setDevice(new QFile(Quasar::config.backendLogFileName));
		logStream_.device()->open(IO_WriteOnly|IO_Truncate);
		
		int i = 0;
		while (i < args.count())
		{
			logStream_ << args[i] << " ";
			++i;
		}
		
		logStream_ << "\n";
		logStream_.device()->flush();
	}

	timer_.start(1000);
	
	emit started();
	
	lastPlayedMedia_ = media;
	isOSDVisible_ = false;
	isTemporaryOSDEnabled_ = false;

	return true;
}

void MPlayer::processReadyReadStdout()
{
	if (process_)
		parseLines();
}

void MPlayer::processExited()
{
	DPRINTF("MPlayer::processExited()");
	if (process_)
		parseLines();
	
	if (!errorMessage_.isEmpty())
		emit errorMessage(errorMessage_);
}

void MPlayer::sendCommand(const QString &c)
{
	if (!isRunning())
		return;
	
	process_->writeToStdin(c + "\n");
	process_->flushStdin();
}

void MPlayer::stop()
{
	if (!isRunning())
		return;

	DPRINTF("MPlayer::stop()");
	
	// timeout timer...
	QTime time;
	time.start();
	bool timedOut = false;

	sendCommand("quit");
	while (process_->isRunning() && !timedOut)
	{
		timedOut = time.elapsed() > 2000;
		usleep(10000); // sleep 10 ms
	}
	
	if (timedOut)
	{
		timedOut = false;
		// try to terminate the process if it hasn't 
		// already exited...
		process_->tryTerminate();
		
		// wait for shutdown, give process time to
		// clean up.
		while (process_->isRunning() && !timedOut)
		{
			timedOut = time.elapsed() > 2000;
			usleep(10000); // sleep 10 ms
		}
		
		// if friendly termination didn't work out,
		// kill the process.
		if (timedOut)
		{
			process_->kill(); // final judgement .
			usleep(10000); // sleep 10 ms
		}
	}
	
	delete process_;
	process_ = NULL;
	
	emit quit();
}

void MPlayer::cleanUp()
{
	if (logStream_.device()) {
		delete logStream_.device();
		logStream_.unsetDevice();
	}

	timer_.stop();
	isPaused_ = false;
	buffer_ = "";
}

void MPlayer::parseLines()
{
	while (process_->canReadLineStdout())
	{
		QString line = process_->readLineStdout();

		if (!errorMessage_.isEmpty())
			errorMessage_ += line + "\n";
		
		if (logStream_.device())
		{
			logStream_ << line << "\n";
			logStream_.device()->flush();
		}
	
		if (line.startsWith("PTS=") && !mplayerHasGetTimePos)
		{
			if (!isSendingPTS_)
			{
				qDebug("mplayer is sending PTS.");
				isSendingPTS_ = true;
			}
			
			int playtime = line.mid(4).toInt();
			emit currentPlayTime(playtime);
		}
		else if (line.startsWith("ANS_TIME_POSITION="))
		{
			QString positionString = line.mid(18);
			int posDot = positionString.find(".");
			int playtime = posDot > -1 ? positionString.left(posDot).toInt() : positionString.toInt();
			emit currentPlayTime(playtime);
		}
		else if (line.startsWith("Exiting..."))
		{
			// ここですぐにemitしてもいい気がする
			if (line.find("Quit") != -1)
			{
				quitState_ = QuitUser;
			}
			else if (line.find("End of file") != -1)
			{
				quitState_ = QuitEOF;
			}
			
			pollMPlayer();
		}
		else if (errorMessage_.isEmpty() && (line.startsWith("Unknown option") || line.startsWith("Cannot find codec")))
		{
			errorMessage_ += line + "\n";
		}
		else if (line.startsWith("Seek failed")) {
			// -hr-mp3-seekのとき、末尾以降にシークしようとするとSeek failedの
			// メッセージを吐きながら帰ってこなくなる。mplayer側の修正が必要。
			stop();
		}

		emit output(line);
	}
}

bool MPlayer::isPaused()
{
	return isPaused_;
}

void MPlayer::pause()
{
	if (!isRunning())
		return;
	
	sendCommand("pause");
	
	if (isPaused_) {
		isPaused_ = false;
		emit resumed();
	} else {
		isPaused_ = true;
		emit paused();
	}
}

void MPlayer::seekRelativeSeconds(int seconds)
{
	QString command;
	sendCommand(command.sprintf("seek %d 0", seconds));
	if (isPaused_)
	    sendCommand("pause");
	
	updatePosition();	
}

void MPlayer::seekAbsolutePercentage(int percent)
{
	QString command;
	sendCommand(command.sprintf("seek %d 1", percent));
	if (isPaused_)
	    sendCommand("pause");
	
	updatePosition();
}

void MPlayer::seekAbsoluteSeconds(int seconds)
{
	QString command;
	sendCommand(command.sprintf("seek %d 2", seconds));
	if (isPaused_)
	    sendCommand("pause");
	
	updatePosition();
}

void MPlayer::osdShowProgressBar(int seconds)
{
	QString command;
	sendCommand(command.sprintf("osd_show_progbar %d", seconds));
}

void MPlayer::toggleOSD()
{
	if (isTemporaryOSDEnabled_)
		return;

	if (isOSDVisible_) {
		isOSDVisible_ = false;
		sendCommand("osd 0");
	} else {
		isOSDVisible_ = true;
		sendCommand("osd 3");
	}
}

void MPlayer::enableTemporaryOSD()
{
	savedOSDVisiblity_ = isOSDVisible_;
	if (!isOSDVisible_)
		toggleOSD();

	isTemporaryOSDEnabled_ = true;
}

void MPlayer::disableTemporaryOSD()
{
	isTemporaryOSDEnabled_ = false;

	if (savedOSDVisiblity_ != isOSDVisible_)
		toggleOSD();
}

void MPlayer::setBrightness(int value)
{
	QString args;
	sendCommand(args.sprintf("brightness %d", value));
}

void MPlayer::pollMPlayer()
{
	if (!isRunning())
	{
		if (errorMessage_.isEmpty() && quitState_ == QuitEOF)
			emit eof();
		else
			emit quit();
	}
	else
		updatePosition();
}

void MPlayer::updatePosition()
{
	if (!isSendingPTS_ && !isPaused_)
	{
		if (mplayerHasGetTimePos)
		{
			sendCommand("get_time_pos");
		}
	}
}

void MPlayer::handleSignal (int sig)
{
	DPRINTF("Got signal %d.", sig);
	stop();
	exit(-sig);
}

void MPlayer::systemVolumeChanged(int level)
{
	sendCurrentVolume();
}

void MPlayer::sendCurrentVolume()
{
	if (isRunning() && useSoftVolume())
	{
		DPRINTF("Setting volume to %d...", qSystemVolume.getCurrentLevel());
		sendCommand("volume " + QString::number(qSystemVolume.getCurrentLevel()) + " 1");
	}
}
