/*****************************************************************************
 *
 *  ffprobe.cpp -- FFprobe interface.
 *
 *  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.
 *
 *****************************************************************************/

#define	RX_PARSE_FFPROBE_SIGNATURE		"(FFprobe)\\s+([^\\s]+)"
#define	RX_PARSE_FFPROBE_FORMAT			"\\[FORMAT\\](.*)\\[/FORMAT\\]"
#define	RX_PARSE_FFPROBE_STREAM			"\\[STREAM\\](.*)\\[/STREAM\\]"

#include "ffprobe.h"
#include <QDebug>
#include <QDir>
#include <QRegExp>
#include <QString>
#include <QStringList>
#include <QStringListIterator>

FFprobe::FFprobe()
{
	m_interfaceName = "FFprobe";
	m_interfaceState = Idle;
}

double FFprobe::fieldStringToDouble(const QString &field)
{
	double value(0.0);

	if (field == "N/A")
	{
		value = 0.0;
	}
	else if (field.contains("."))
	{
		value = field.toDouble();
	}
	else if (field.contains("/"))
	{
		QRegExp rx("(\\d+)/(\\d+)");
		rx.setMinimal(false);
		rx.indexIn(field);
		if (rx.numCaptures() == 2 && rx.cap(1).toInt() > 0 && rx.cap(2).toInt() > 0)
		{
			QStringList operandes = field.split("/");		

			value = operandes[0].toDouble() / operandes[1].toDouble();
		}
	}
	else if (field.contains(":"))
	{
		QRegExp rx("(\\d+):(\\d+)");
		rx.setMinimal(false);
		rx.indexIn(field);
		if (rx.numCaptures() == 2 && rx.cap(1).toInt() > 0 && rx.cap(2).toInt() > 0)
		{
			QStringList operandes = field.split(":");		

			value = operandes[0].toDouble() / operandes[1].toDouble();
		}
	}

	return value;
}
bool FFprobe::getDataFormat(QString *value, const QString &field)
{
	if (m_inputFile.isEmpty())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no file loaded"));
		
		qDebug() << "FFprobe: no file loaded" << m_interfacePath;
	
		value->clear();
		return false;
	}

	if (!m_dataReady)
	{
		m_console->log(Console::Verbose, tr("FFprobe: data not ready for file %1").arg(m_inputFile));
		
		qDebug() << "FFprobe: format data not ready for" << m_inputFile << m_interfacePath;

		value->clear();
		return false;
	}

	bool ok;
	QString probeValue = parseDataField(field, &m_dataFormat, &ok);

	if (!ok)
	{
		value->clear();

		m_console->log(Console::Verbose, tr("FFprobe: '%1' not found for file %2")
			.arg(field).arg(m_inputFile));
		
		qDebug() << "FFprobe: format data not found" << field << m_inputFile << m_interfacePath;
	}
	else
	{
		*value = probeValue;

		m_console->log(Console::Verbose, tr("FFprobe: '%1' is '%2' for file %3")
			.arg(field).arg(probeValue).arg(m_inputFile));

		qDebug() << "FFprobe: format data" << field << probeValue << m_inputFile << m_interfacePath;
	}

	return ok;
}

int FFprobe::getDataFormat(QHash<QString, QString> *valueList, 
		QStringList *fieldList, QStringList *fieldNotFound)
{
	valueList->clear();

	if (m_inputFile.isEmpty())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no file loaded"));
		
		qDebug() << "FFprobe: no file loaded" << m_interfacePath;

		if (fieldNotFound)  *fieldNotFound = *fieldList;
		return 0;
	}

	if (!m_dataReady)
	{
		m_console->log(Console::Verbose, tr("FFprobe: data not ready for file %1").arg(m_inputFile));

		qDebug() << "FFprobe: data not ready for" << m_inputFile << m_interfacePath;

		if (fieldNotFound)  *fieldNotFound = *fieldList;
		return 0;
	}

	if (fieldNotFound) fieldNotFound->clear();

	QStringListIterator i(*fieldList);
	while (i.hasNext())
	{
		bool ok;
		QString field = i.next();

		QString value = parseDataField(field, &m_dataFormat, &ok);
		
		if (!ok) 
		{
			if (fieldNotFound) fieldNotFound->append(field);
		}
		else
		{
			valueList->insert(field, value);
		}
	}

	return valueList->count();
}

bool FFprobe::getDataStream(const int streamIndex, QString *value, const QString &field)
{
	if (m_inputFile.isEmpty())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no file loaded"));
		
		qDebug() << "FFprobe: no file loaded" << m_interfacePath;
	
		value->clear();
		return false;
	}

	if (!m_dataReady)
	{
		m_console->log(Console::Verbose, tr("FFprobe: data not ready for file %1").arg(m_inputFile));
		
		qDebug() << "FFprobe: data not ready for" << m_inputFile << m_interfacePath;

		value->clear();
		return false;
	}

	if (streamIndex >= m_dataStream.count())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no data ready for stream #%1 in file %2")
			.arg(streamIndex).arg(m_inputFile));
		
		qDebug() << "FFprobe: no data ready for stream" << streamIndex << m_inputFile << m_interfacePath;

		value->clear();
		return false;
	}

	bool ok;
	QString probeValue = parseDataField(field, &m_dataStream[streamIndex], &ok);

	if (!ok)
	{
		value->clear();

		m_console->log(Console::Verbose, tr("FFprobe: '%1' not found for stream %2 in file %3")
			.arg(field).arg(streamIndex).arg(m_inputFile));
		
		qDebug() << "FFprobe: data not found for stream" << streamIndex << field << m_inputFile << m_interfacePath;
	}
	else
	{
		*value = probeValue;
	}

	return ok;
}

int FFprobe::getDataStream(const int streamIndex, QHash<QString, QString> *valueList, 
		QStringList *fieldList, QStringList *fieldNotFound)
{
	valueList->clear();

	if (m_inputFile.isEmpty())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no file loaded"));
		
		qDebug() << "FFprobe: no file loaded" << m_interfacePath;
	
		if (fieldNotFound) *fieldNotFound = *fieldList;
		return 0;
	}

	if (!m_dataReady)
	{
		m_console->log(Console::Verbose, tr("FFprobe: data not ready for file %1").arg(m_inputFile));
		
		qDebug() << "FFprobe: data not ready for" << m_inputFile << m_interfacePath;

		if (fieldNotFound) *fieldNotFound = *fieldList;
		return 0;
	}

	if (streamIndex >= m_dataStream.count())
	{
		m_console->log(Console::Verbose, tr("FFprobe: no data ready for stream #%1 in file %2")
			.arg(streamIndex).arg(m_inputFile));
		
		qDebug() << "FFprobe: no data ready for stream" << streamIndex << m_inputFile << m_interfacePath;

		if (fieldNotFound) *fieldNotFound = *fieldList;
		return 0;
	}

	if (fieldNotFound) fieldNotFound->clear();

	QStringListIterator i(*fieldList);
	while (i.hasNext())
	{
		bool ok;
		QString field = i.next();

		QString value = parseDataField(field, &m_dataStream[streamIndex], &ok);
		
		if (!ok) 
		{
			if (fieldNotFound) fieldNotFound->append(field);
		}
		else
		{
			valueList->insert(field, value);
		}
	}

	return valueList->count();
}

QList<int> FFprobe::getStreamIndexesAttachment()
{
	return m_streamIndexesAttachment;
}

QList<int> FFprobe::getStreamIndexesAudio()
{
	return m_streamIndexesAudio;
}

QList<int> FFprobe::getStreamIndexesData()
{
	return m_streamIndexesData;
}

QList<int> FFprobe::getStreamIndexesSubtitle()
{
	return m_streamIndexesSubtitle;
}

QList<int> FFprobe::getStreamIndexesVideo()
{
	return m_streamIndexesVideo;
}

int	 FFprobe::getStreamNumber()
{
	return m_dataStream.count();
}

void FFprobe::interfaceReset()
{
	m_interfaceState = Idle;
}

bool FFprobe::interfaceStart(const QString &path, const QString &libPath)  // IProcessor interface
{
	if (path.isEmpty()) return m_interfaceState;

	if (m_interfacePath == path)
	{
		if (m_interfaceState == Failed) return false;
		if (m_interfaceState == Ready) return true;
	}

	m_interfaceState = Idle;
	m_interfacePath = path.toLatin1();
	m_interfaceLibPath = libPath;
	
	m_console->log(Console::Verbose, tr("FFprobe: start interface."));
	qDebug() << "FFprobe: start interface" << m_interfacePath;

	QString err, stdErr, stdOut;
	QStringList args;

	// Get FFprobe version
	args = QStringList() << "-version";

	if (!run(&args, &stdOut, &stdErr, &err))
	{
		m_console->log(tr("FFprobe: %1.").arg(err));
		qDebug() << "FFprobe: failed to start interface" << err << m_interfacePath;
		m_interfaceState = Failed;
		return false;
	}

	m_console->stdOut(stdOut);
	m_console->stdErr(stdErr);

	parseInterfaceVersion(&stdOut);
	
	m_console->log(Console::Verbose, tr("FFprobe: interface ready."));
	qDebug() << "FFprobe: interface ready" << m_interfacePath;
	m_interfaceState = Ready;
	return true;
}

bool FFprobe::load(const QString &mediaFile)
{
	m_console->log(Console::Verbose, tr("FFprobe: load %1").arg(mediaFile));

	m_dataReady = false;
	m_dataFormat.clear();
	m_dataStream.clear();
	m_streamIndexesAttachment.clear();
	m_streamIndexesAudio.clear();
	m_streamIndexesData.clear();
	m_streamIndexesSubtitle.clear();
	m_streamIndexesVideo.clear();

	m_inputFile = mediaFile;
	
	if (m_interfaceState != Ready) return false;

	qDebug() << "FFprobe: load" << m_inputFile << m_interfacePath;

	QString err, stdErr, stdOut;
	QStringList args;

	// Run FFprobe on media file
	args = QStringList() 
		<< "-analyzeduration" << "1000" 
		<< "-show_format" 
		<< "-show_streams" 
		<< mediaFile;

	if (!run(&args, &stdOut, &stdErr, &err))
	{
		m_console->log(Console::Quiet, tr("FFprobe: process error loading %1 '%2'")
			.arg(m_inputFile).arg(err));

		qDebug() << "FFprobe: run error" << err << m_inputFile << m_interfacePath;
		return false;
	}

	m_console->stdOut(stdOut);
	m_console->stdErr(stdErr);

	//[FORMAT]
	//filename=\Users\jm\Videos\THE_DARK_KNIGHT VO (1).part1.avi
	//nb_streams=2
	//format_name=avi
	//format_long_name=AVI format
	//start_time=0.000000
	//duration=6078.240000
	//size=775445608.000000
	//bit_rate=1020618.000000
	//TAG:encoder=MEncoder Sherpya-SVN-r33216-4.2.5
	//[/FORMAT]
	//[FORMAT]
	//filename=\Users\jm\Videos\THE_DARK_KNIGHT VO (1).part1.avi
	//nb_streams=2
	//format_name=avi
	//format_long_name=AVI format
	//start_time=0.000000
	//duration=6078.240000
	//size=775445608.000000
	//bit_rate=1020618.000000
	//TAG:encoder=MEncoder Sherpya-SVN-r33216-4.2.5
	//[/FORMAT]

	QRegExp rx(RX_PARSE_FFPROBE_FORMAT);
	rx.setMinimal(true);
	rx.indexIn(stdOut);
	if ( rx.numCaptures() != 1 || rx.cap(1).length() == 0 )
	{
		m_console->log(Console::Quiet, tr("FFprobe: parse error loading %1 '%2'")
			.arg(m_inputFile).arg(tr("can't parse format, input may not be a multimedia file")));
		
		qDebug() << "FFprobe: failed to parse format" << m_inputFile << m_interfacePath;
		return false;
	}

	m_dataFormat =  rx.cap(1).trimmed();

	//[STREAM]
	//index=0
	//codec_name=mpeg1video
	//codec_long_name=MPEG-1 video
	//codec_type=video
	//codec_time_base=1/25
	//codec_tag_string=[0][0][0][0]
	//codec_tag=0x0000
	//width=352
	//height=288
	//has_b_frames=1
	//sample_aspect_ratio=64:45
	//display_aspect_ratio=704:405
	//pix_fmt=yuv420p
	//id=0x1e0
	//r_frame_rate=25/1
	//avg_frame_rate=25/1
	//time_base=1/90000
	//start_time=0.880000
	//duration=8756.840000
	//[/STREAM]
	//[STREAM]
	//index=1
	//codec_name=mp2
	//codec_long_name=MP2 (MPEG audio layer 2)
	//codec_type=audio
	//codec_time_base=0/1
	//codec_tag_string=[0][0][0][0]
	//codec_tag=0x0000
	//sample_rate=44100.000000
	//channels=2
	//bits_per_sample=0
	//id=0x1c0
	//r_frame_rate=0/0
	//avg_frame_rate=25151/657
	//time_base=1/90000
	//start_time=0.880000
	//duration=8756.924078
	//[/STREAM]

	rx.setPattern(RX_PARSE_FFPROBE_STREAM);
	int pos = 0;
	while ((pos = rx.indexIn(stdOut, pos)) != -1) 
	{
		m_dataStream <<  rx.cap(1).trimmed();

		pos += rx.matchedLength() - 1;
	}

	m_dataReady = true;
	
	if (m_dataStream.isEmpty())
	{
		m_console->log(Console::Default, tr("FFprobe: no stream found in file %1")
			.arg(m_inputFile));
		
		qDebug() << "FFprobe: failed to parse streams" << m_inputFile << m_interfacePath;
	}

	rx.setPattern("index=(\\d+)[^\\d].*codec_type=(v|a|s|d|t)");
	rx.setMinimal(true);
	QStringListIterator i(m_dataStream);
	while (i.hasNext())
	{
		QString data = i.next();

		rx.indexIn(data);
		if (rx.numCaptures() != 2 || rx.cap(1).length() == 0 || rx.cap(2).length() == 0)
		{
			if (rx.cap(1).length() > 0)
			{
				m_console->log(Console::Verbose, tr("FFprobe: undefined stream #%1 of %2")
					.arg(rx.cap(1)).arg(m_inputFile));
				
				qDebug() << "FFprobe: undefined stream" << rx.cap(1) << m_inputFile << m_interfacePath;
			}
			else
			{
				m_console->log(Console::Verbose, tr("FFprobe: undefined stream in %2")
					.arg(m_inputFile));
				
				qDebug() << "FFprobe: undefined stream" << rx.cap(1) << rx.cap(2) << m_inputFile << m_interfacePath;
			}
		}
		else
		{
			int streamIndex = rx.cap(1).toInt();
			QString streamType = rx.cap(2);

			if (streamType == "t")		m_streamIndexesAttachment << streamIndex;
			else if (streamType == "a")	m_streamIndexesAudio << streamIndex;
			else if (streamType == "d")	m_streamIndexesData << streamIndex;
			else if (streamType == "s")	m_streamIndexesSubtitle << streamIndex;
			else if (streamType == "v")	m_streamIndexesVideo << streamIndex;
		}
	}

	qDebug() << "FFprobe: load complete" << m_inputFile << m_interfacePath;
	return true;
}

QString FFprobe::parseDataField(const QString &field, QString *data, bool *ok)
{
	//format_name=mov,mp4,m4a,3gp,3g2,mj2
	//format_long_name=AVI format
	//duration=6078.240000
	//...

#ifdef WIN32
	QString regex = QString("%1=(.+)\\r\\n").arg(field);
#else
	QString regex = QString("%1=(.+)\\n").arg(field);
#endif

	QRegExp rx(regex);
	rx.setCaseSensitivity(Qt::CaseInsensitive);
	rx.setMinimal(true);
	rx.indexIn(*data);
	if (rx.numCaptures() != 1 || rx.cap(1).length() == 0)
	{
		m_console->log(Console::Verbose, tr("FFprobe: failed to parse '%1' of %2")
			.arg(field).arg(m_inputFile));
		qDebug() << "FFprobe: failed to parse" << field << rx.cap(1) << m_inputFile << m_interfacePath;
		*ok = false;
		return QString();
	}
	else
	{
		qDebug() << "FFprobe: parse success" << field << rx.cap(1).trimmed() << m_inputFile;
		*ok = true;
		return rx.cap(1).trimmed();
	}
}

void FFprobe::parseInterfaceVersion(QString *data)
{
	QRegExp rx(RX_PARSE_FFPROBE_SIGNATURE);
	rx.setCaseSensitivity(Qt::CaseInsensitive);
	rx.setMinimal(false);
	rx.indexIn(*data);
	if (rx.numCaptures() != 2 || rx.cap(1).length() == 0 || rx.cap(2).length() == 0)
	{
		m_console->log(Console::Verbose, tr("FFprobe: failed to parse signature."));
		qDebug() << "FFprobe: failed to parse signature" << m_interfacePath;
		return;
	}

	m_interfaceExec = rx.cap(1);
	m_interfaceVersion = rx.cap(2);
}
