/*
 * Copyright (C) 2006-2007	Andre Beckedorf
 * 					 		<evilJazz _AT_ katastrophos _DOT_ 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 <cstdio>
#include <cstdlib>
#include <unistd.h>

#include <qstring.h>
#include <qdatastream.h>
#include <qtextcodec.h>
#include <qregexp.h>

#include "mediaidentifier.h"
#include "mediadatabase.h"
#include "mplayer.h"
#include "quasar.h"
#include "configuration.h"
#include "debug.h"

#include <id3/tag.h>
#include <id3/utils.h>
#include <id3/misc_support.h>
#include <id3/readers.h>
#include <id3/io_helpers.h>


MediaIdentifier & MediaIdentifier::singleton()
{
	static MediaIdentifier instance;
	return instance;
}


MediaIdentifier::MediaIdentifier()
	: media_(NULL),
	  fileScanningFinished_(false),
	  identifyProcess_(NULL),
	  tempLine_("")
{
	connect(&killIdentifyProcessTimer_, SIGNAL(timeout()), this, SLOT(killIdentifyProcessTimerTimedOut()));
}

MediaIdentifier::~MediaIdentifier()
{
	DPRINTF("Killing mplayer slave because we're quitting...");
	cleanUpIdentifyProcess();
}

void MediaIdentifier::cleanUpIdentifyProcess()
{
	if (identifyProcess_)
	{
		identifyProcess_->tryTerminate();
		usleep(1000000); // wait 1 sec
		if (identifyProcess_->isRunning())
		{
			identifyProcess_->kill();
			usleep(500000); // wait 500 ms
		}
		
		delete identifyProcess_;
		identifyProcess_ = NULL;
	}
}

void MediaIdentifier::killIdentifyProcessTimerTimedOut()
{
	killIdentifyProcessTimer_.stop();
	DPRINTF("Killing mplayer slave since we no longer need it...");
	cleanUpIdentifyProcess();
}

void MediaIdentifier::sendCommand(const QString &command)
{
	identifyProcess_->writeToStdin(command + "\n");
	identifyProcess_->flushStdin();
}

void MediaIdentifier::identify(Media *media, const MediaLocation &location)
{
	// kill mplayer slave after 10 seconds, if no other identification request
	// happens within that period...
	killIdentifyProcessTimer_.changeInterval(10000);
	
	// perhaps we are retrying after a timeout?
	// when this happens, media_ is still set, so use this information to
	// recognize this state...
	bool mediaWasSet = media_;
	
	media_ = media;	
	fileScanningFinished_ = false;
	
	if (!identifyProcess_ || !identifyProcess_->isRunning())
	{
		QStringList args;
		args << Quasar::config.mplayerPath << "-vo" << "null" << "-ao" << "null" << "-v" << "-nocache" << "-slave" << "-identify";
		args << location.toString();
	
		identifyProcess_ = new QProcess(args);
	
		DPRINTF("Starting mplayer slave...");
		identifyProcess_->start();
		
		DPRINTF("Sending pause...");
		sendCommand("pause");
	}
	else
	{
		DPRINTF("Mplayer slave still running, inputing new file...");
		sendCommand("pausing loadfile \"" + location.toString() + "\" 0");
	}
	
	DPRINTF("Waiting for file scanning to finish...");
	
	// timeout timer...
	QTime time;
	time.start();
	bool timedOut = false;
	
	while ((identifyProcess_->isRunning() || identifyProcess_->canReadLineStdout()) && !fileScanningFinished_ && !timedOut)
	{
		timedOut = time.elapsed() > 5000;
		parseLines();
		usleep(1000); // sleep 1 ms
	}

	if (timedOut)
	{
		DPRINTF("Timeout while scanning file...");
		cleanUpIdentifyProcess();
		
		if (!mediaWasSet)
		{
			DPRINTF("Retrying again...");
			identify(media, location);
		}
		
		return;
	}
	else
	{
		if (location.isLocalFile() && QFile::exists(location.toString()))
			loadTags(location);
	}
			
	media_ = NULL;
}

bool containsUTF8(const QString &input)
{
	// This uses many REs instead of just one because Qt2 doesn't support the | operator in REs...
	// This might be a bit faster too...	
	
	// Check for non-overlong 2 byte long sequences...
	if (QRegExp("[\x00C2-\x00DF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for sequences excluding overlongs...
	if (QRegExp("\x00E0[\x00A0-\x00BF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for straight 3 byte long sequences...
	if (QRegExp("[\x00E1-\x00EC\x00EE\x00EF][\x0080-\x00BF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for sequences excluding surrogates...
	if (QRegExp("\x00ED[\x0080-\x009F][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for planes 1-3 sequences...
	if (QRegExp("\x00F0[\x0090-\x00BF][\x0080-\x00BF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for planes 4-15 sequences...
	if (QRegExp("[\x00F1-\x00F3][\x0080-\x00BF][\x0080-\x00BF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// Check for planes 16 sequences...
	if (QRegExp("\x00F4[\x0080-\x008F][\x0080-\x00BF][\x0080-\x00BF]").match(input) > -1) return true;
	
	// The input string does not contain UTF-8 sequences...
	return false;
}

QString reinterpretUTF8(const QString &input)
{
	return QString::fromUtf8(input.latin1(), input.length());
}

void MediaIdentifier::parseLines()
{
	if (!media_)
		return;
	
	while (identifyProcess_->canReadLineStdout())
	{
		QString line = identifyProcess_->readLineStdout();
		
		DPRINTF("line: %s", (const char *)line.utf8());

		if (line.startsWith("ID_"))
		{
			// ID_*
			line = line.mid(3);
			if (line.startsWith("LENGTH="))
			{
				line = line.mid(7);
				int posDot = line.find(".");
				media_->mediaMetadata()->length = (posDot > -1 ? line.left(posDot).toInt() : line.toInt());
			}
			else if (line.startsWith("AUDIO_"))
			{
				// ID_AUDIO_*
				line = line.mid(6);
				media_->mediaFileInfo()->mediaType = MediaFileInfo::MEDIATYPE_AUDIO;
				if (line.startsWith("CODEC="))
					media_->mediaAudio()->codec = line.mid(6);
				else if (line.startsWith("FORMAT="))
					media_->mediaAudio()->format = line.mid(7).toInt();
				else if (line.startsWith("BITRATE="))
					media_->mediaAudio()->bitrate = line.mid(8).toInt();
				else if (line.startsWith("RATE="))
					media_->mediaAudio()->sampleRate = line.mid(5).toInt();
				else if (line.startsWith("NCH="))
					media_->mediaAudio()->channels = line.mid(4).toInt();
			} 
			else if (line.startsWith("VIDEO_"))
			{
				// ID_VIDEO_*
				line = line.mid(6);
				media_->mediaFileInfo()->mediaType = MediaFileInfo::MEDIATYPE_VIDEO;
				if (line.startsWith("FORMAT="))
					media_->mediaVideo()->codec = line.mid(7);
				else if (line.startsWith("BITRATE="))
					media_->mediaVideo()->bitrate = line.mid(8).toInt();
				else if (line.startsWith("WIDTH="))
					media_->mediaVideo()->width = line.mid(6).toInt();
				else if (line.startsWith("HEIGHT="))
					media_->mediaVideo()->height = line.mid(7).toInt();
				else if (line.startsWith("FPS="))
					media_->mediaVideo()->FPS = line.mid(4).toFloat();
				else if (line.startsWith("ASPECT="))
					media_->mediaVideo()->aspectRatio = line.mid(7).toFloat();
			}
			else if (line.startsWith("CLIP_INFO_NAME"))
			{
				tempLine_ = "";
				
				int posEq = line.find('=');
				if (posEq > -1)
					tempLine_ = line.mid(posEq + 1);
			}			
			else if (line.startsWith("CLIP_INFO_VALUE") && tempLine_.length() > 0)
			{
				int posEq = line.find('=');
				if (posEq > -1)
				{
					line = line.mid(posEq + 1);
					
					if (containsUTF8(line))
						line = reinterpretUTF8(line);
					
					addMediadata(tempLine_, line);
				}
			}
		}
		else if (line.startsWith("OggVorbisComment: "))
		{
			media_->mediaFileInfo()->mediaType = MediaFileInfo::MEDIATYPE_AUDIO;
			line = line.mid(18);
			int sepIndex = line.find('=');
			if (sepIndex > 0)
			{
				QString tag(line.left(sepIndex));
				QString value(line.mid(sepIndex + 1));

				if (containsUTF8(value))
					value = reinterpretUTF8(value);

				addMediadata(tag, value);
			}
		} 
		else if (line.startsWith("SLAVE: Couldn't open demuxer"))
		{
			media_->mediaFileInfo()->mediaType = MediaFileInfo::MEDIATYPE_UNKNOWN;
		}
		else if (line.startsWith("Starting playback"))
		{
			//qDebug("Got finish signal...");
			fileScanningFinished_ = true;
		}
	}
}

void MediaIdentifier::addMediadata(const QString &tag, const QString &value)
{
	MediaMetadataExt * mediametadata = media_->mediaMetadataExt();

	// Resolve common aliases...
	QCString key = QCString(tag.upper());
	if (key == "NAME")
		key = "TITLE";
	else if (key == "TRACKNUMBER")
		key = "TRACK";

	// Check if the key exists. If not, add it.
	MetadataMap::Iterator it = mediametadata->map.find(key);
	if (it == mediametadata->map.end())
		it = mediametadata->map.insert(key, MetadataEntry());

	// Try to avoid duplicates...
	if (!it.data().contains(value))
		it.data().append(value);
}

bool MediaIdentifier::loadTags(const MediaLocation &location)
{
	QString filename(location.toString());

	if (QFileInfo(filename).extension(false).find("mp3", 0, false) != -1) 
	{
		QTextCodec *codec = QTextCodec::codecForName("SJIS");
		
		ID3_Tag myTag;
		myTag.Link(filename.utf8(), ID3TT_ALL);

		MediaMetadataExt * mediametadata = media_->mediaMetadataExt();

		ID3_Tag::Iterator* iter = myTag.CreateIterator();
		const ID3_Frame* frame = NULL;
		while (NULL != (frame = iter->GetNext()))
		{
			ID3_FrameID eFrameID = frame->GetID();
			switch (eFrameID)
			{
				case ID3FID_TITLE:
				{
					char * sText = ID3_GetString(frame, ID3FN_TEXT);
					addMediadata("TITLE", codec->toUnicode(sText));
					delete [] sText;
					break;				
				}
				case ID3FID_ALBUM:
				{
					char * sText = ID3_GetString(frame, ID3FN_TEXT);
					addMediadata("ALBUM", codec->toUnicode(sText));
					delete [] sText; 
					break;				
				}
				case ID3FID_BAND:
				case ID3FID_ORIGARTIST:
				case ID3FID_LEADARTIST:
				{
					char * sText = ID3_GetString(frame, ID3FN_TEXT);
					addMediadata("ARTIST", codec->toUnicode(sText));
					delete [] sText;
					break;				
				}
				case ID3FID_TRACKNUM:
				{
					char * sText = ID3_GetString(frame, ID3FN_TEXT);
					addMediadata("TRACK", codec->toUnicode(sText));
					delete [] sText;		
					break;
				}
				case ID3FID_COMMENT:
				{
					char * sText = ID3_GetString(frame, ID3FN_TEXT);
					addMediadata("COMMENT", codec->toUnicode(sText));
					delete [] sText;
					break;
				}
				default:
				{
					break;
				}
			}
		}
		delete iter;
		return true;	
	}
	return false;
}
