/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2013  Georg Rudoy
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

#include "chroma.h"
#include <stdexcept>
#include <memory>
#include <QString>
#include <QByteArray>
#include <QtDebug>

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/audioconvert.h>
#include <libavutil/samplefmt.h>
}

namespace LeechCraft
{
namespace MusicZombie
{
	QMutex Chroma::CodecMutex_ (QMutex::NonRecursive);
	QMutex Chroma::RegisterMutex_ (QMutex::NonRecursive);

	Chroma::Chroma ()
	: Ctx_ (chromaprint_new (CHROMAPRINT_ALGORITHM_DEFAULT))
	{
		QMutexLocker locker (&RegisterMutex_);
		av_register_all ();
	}

	Chroma::~Chroma ()
	{
		chromaprint_free (Ctx_);
	}

	Chroma::Result Chroma::operator() (const QString& filename)
	{
		std::shared_ptr<AVFormatContext> formatCtx;
		{
			AVFormatContext *formatCtxRaw = nullptr;
			if (avformat_open_input (&formatCtxRaw, filename.toLatin1 ().constData (), nullptr, nullptr))
				throw std::runtime_error ("error opening file");

			formatCtx.reset (formatCtxRaw,
					[] (AVFormatContext *ctx) { avformat_close_input (&ctx); });
		}

		{
			QMutexLocker locker (&CodecMutex_);
			if (av_find_stream_info (formatCtx.get ()) < 0)
				throw std::runtime_error ("could not find stream");
		}

		bool codecOpened = false;
		std::shared_ptr<AVCodecContext> codecCtx;
		AVStream *stream = nullptr;
		for (unsigned int i = 0; i < formatCtx->nb_streams; ++i)
		{
			codecCtx.reset (formatCtx->streams [i]->codec,
					[&codecOpened, this] (AVCodecContext *ctx)
					{
						if (codecOpened)
						{
							QMutexLocker locker (&CodecMutex_);
							avcodec_close (ctx);
						}
					});
			if (codecCtx && codecCtx->codec_type == AVMEDIA_TYPE_AUDIO)
			{
				stream = formatCtx->streams [i];
				break;
			}
		}

		if (!stream)
			throw std::runtime_error ("could not find stream");

		auto codec = avcodec_find_decoder (codecCtx->codec_id);
		if (!codec)
			throw std::runtime_error ("unknown codec");

		{
			QMutexLocker locker (&CodecMutex_);
			if (avcodec_open2 (codecCtx.get (), codec, nullptr) < 0)
				throw std::runtime_error ("couldn't open the codec");
		}
		codecOpened = true;

		if (codecCtx->channels <= 0)
			throw std::runtime_error ("no channels found");

		/* TODO swresample
		 *
		 * Upstream ffmpeg/libav have migrated to it.
		 * https://github.com/xbmc/xbmc/pull/882
		 */
		if (codecCtx->sample_fmt != AV_SAMPLE_FMT_S16)
			throw std::runtime_error ("invalid sampling format");

		AVPacket packet, tmpPacket;
		av_init_packet (&packet);
		av_init_packet (&tmpPacket);

		const int maxLength = 120;
		auto remaining = maxLength * codecCtx->channels * codecCtx->sample_rate;
		chromaprint_start (Ctx_, codecCtx->sample_rate, codecCtx->channels);

		const int bufferSize = AVCODEC_MAX_AUDIO_FRAME_SIZE * 2;
		int16_t *buffer = static_cast<int16_t*> (av_malloc (bufferSize + 16));

		while (true)
		{
			if (av_read_frame (formatCtx.get (), &packet) < 0)
				break;

			tmpPacket.data = packet.data;
			tmpPacket.size = packet.size;

			bool finished = false;
			while (tmpPacket.size > 0)
			{
				auto bufferUsed = bufferSize;
				auto consumed = avcodec_decode_audio3 (codecCtx.get (), buffer, &bufferUsed, &tmpPacket);
				if (consumed < 0)
					break;

				tmpPacket.data += consumed;
				tmpPacket.size -= consumed;

				if (bufferUsed <= 0 || bufferUsed >= bufferSize)
				{
					if (bufferUsed)
						qWarning () << "invalid size returned";
					continue;
				}

				const auto length = std::min (remaining, bufferUsed / 2);
				if (!chromaprint_feed (Ctx_, buffer, length))
					throw std::runtime_error ("fingerprint calculation failed");

				if (maxLength)
				{
					remaining -= length;
					if ((finished = (remaining <= 0)))
						break;
				}
			}

			if (packet.data)
				av_free_packet (&packet);

			if (finished)
				break;
		}

		if (!chromaprint_finish (Ctx_))
			throw std::runtime_error ("fingerprint calculation failed");

		char *fingerprint = 0;
		if (!chromaprint_get_fingerprint (Ctx_, &fingerprint))
			throw std::runtime_error ("unable to get fingerprint");

		QByteArray result (fingerprint);
		chromaprint_dealloc (fingerprint);

		const double divideFactor = 1. / av_q2d (stream->time_base);
		const double duration = stream->duration / divideFactor;

		return { result, static_cast<int> (duration) };
	}
}
}
