/*
 * \brief  Audio player
 * \author Josef Soentgen
 * \date   2015-11-19
 */

/*
 * Copyright (C) 2015-2016 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU General Public License version 2.
 */

/* Genode includes */
#include <base/attached_rom_dataspace.h>
#include <libc/component.h>
#include <base/heap.h>
#include <base/sleep.h>
#include <os/reporter.h>
#include <util/retry.h>
#include <util/xml_node.h>
#include <audio_out_session/connection.h>

/* local includes */
#include <list.h>
#include <ring_buffer.h>

extern "C" {
/*
 * UINT64_C is needed by libav headers
 *
 * Use the compiler's definition as fallback because the UINT64_C macro is only
 * defined in <machine/_stdint.h> when used with C.
 */
#ifndef UINT64_C
#define UINT64_C(c) __UINT64_C(c)
#endif

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/dict.h>
#include <libavutil/frame.h>
#include <libavutil/opt.h>
#include <libavresample/avresample.h>

#include <fcntl.h>
#include <unistd.h>
}; /* extern "C" */


namespace Audio_player {
	class  Output;
	class  Playlist;
	class  Decoder;
	struct Main;

	typedef Util::Ring_buffer<64 * 1024> Frame_data;
	typedef Genode::String<1024>         Path;

	enum { LEFT, RIGHT, NUM_CHANNELS };
	enum { AUDIO_OUT_PACKET_SIZE = Audio_out::PERIOD * NUM_CHANNELS * sizeof(float) };
	enum { QUEUED_PACKET_THRESHOLD = 10 };
}

struct Audio_player::Output
{
	Audio_out::Connection  _left;
	Audio_out::Connection  _right;
	Audio_out::Connection *_out[NUM_CHANNELS];

	Audio_out::Packet     *_alloc_position;

	unsigned _packets_submitted = 0;

	template <typename FUNC>
	static void for_each_channel(FUNC const &func) {
		for (int i = 0; i < Audio_player::NUM_CHANNELS; i++) func(i); }

	/**
	 * Constructor
	 *
	 * \param sigh  progress signal handler
	 */
	Output(Genode::Env &env,
	       Genode::Signal_context_capability sigh)
	: _left(env, "left", true, true), _right(env, "right", false, false)
	{
		/*
		 * We only care about the left (first) channel and sync all other
		 * channels with it when needed
		 */
		_left.progress_sigh(sigh);

		_out[LEFT]  = &_left;
		_out[RIGHT] = &_right;
	}

	void start()
	{
		for_each_channel([&] (int const i) { _out[i]->start(); });
	}

	void stop()
	{
		for_each_channel([&] (int const i) { _out[i]->stop(); });
		_alloc_position = nullptr;
	}

	/**
	 * Return rounded estimate of packets per secound
	 */
	unsigned packets_per_sec() const { return Audio_out::SAMPLE_RATE / Audio_out::PERIOD; }

	/**
	 * Return the size of one output frame
	 */
	size_t frame_size() const { return AUDIO_OUT_PACKET_SIZE; }

	/**
	 * Return number of currently queued packets in Audio_out stream
	 */
	unsigned queued()
	{
		if (_alloc_position == nullptr) _alloc_position = _out[LEFT]->stream()->next();

		unsigned const packet_pos = _out[LEFT]->stream()->packet_position(_alloc_position);
		unsigned const play_pos   = _out[LEFT]->stream()->pos();
		unsigned const queued     = packet_pos < play_pos
		                            ? ((Audio_out::QUEUE_SIZE + packet_pos) - play_pos)
		                            : packet_pos - play_pos;
		return queued;
	}

	/**
	 * Fetch decoded frames from frame data buffer and fill Audio_out packets
	 */
	void drain_buffer(Frame_data &frame_data)
	{
		if (_alloc_position == nullptr) _alloc_position = _out[LEFT]->stream()->next();

		while (frame_data.read_avail() > (AUDIO_OUT_PACKET_SIZE)) {
			Audio_out::Packet *p[NUM_CHANNELS];

			p[LEFT] = _out[LEFT]->stream()->next(_alloc_position);

			unsigned const ppos = _out[LEFT]->stream()->packet_position(p[LEFT]);
			p[RIGHT]            = _out[RIGHT]->stream()->get(ppos);

			float tmp[Audio_out::PERIOD * NUM_CHANNELS];
			size_t const n = frame_data.read(tmp, sizeof(tmp));

			if (n != sizeof(tmp)) {
				Genode::warning("less frame data read than expected");
			}

			float *left_content  = p[LEFT]->content();
			float *right_content = p[RIGHT]->content();

			for (unsigned i = 0; i < Audio_out::PERIOD; i++) {
					left_content[i]  = tmp[i * NUM_CHANNELS + LEFT];
					right_content[i] = tmp[i * NUM_CHANNELS + RIGHT];
			}

			for_each_channel([&] (int const i) { _out[i]->submit(p[i]); });

			_alloc_position = p[LEFT];

			_packets_submitted++;
		}
	}

	/**
	 * Return number of already submitted Audio_out packets
	 */
	unsigned packets_submitted() { return _packets_submitted; }
};


class Audio_player::Playlist
{
	public:

		struct Track : public Util::List<Track>::Element
		{
			Path     path;
			unsigned id = 0;

			Track() { }
			Track(char const *path, unsigned id) : path(path), id(id) { }
			Track(Track const &track) : path(track.path), id(track.id) { }

			bool valid() { return path.length(); }
		};

		template <typename FUNC>
		void for_each_track(FUNC const &func) {
			for (Track *t = _track_list.first(); t; t = t->next())
				func(*t);
		}

	private:

		Genode::Allocator &_alloc;
		Util::List<Track>  _track_list;
		Track             *_curr_track = nullptr;

		typedef enum { MODE_ONCE, MODE_REPEAT } Mode;
		Mode _mode;

		void _insert(char const *path, unsigned id)
		{
			try {
				Track *t = new (&_alloc) Track(path, id);
				_track_list.append(t);
			} catch (...) { Genode::warning("could not insert track"); }
		}

		void _remove_all()
		{
			while (Track *t = _track_list.first()) {
				_track_list.remove(t);
				Genode::destroy(&_alloc, t);
			}
		}

	public:

		Playlist(Genode::Allocator &alloc) : _alloc(alloc), _mode(MODE_ONCE) { }

		~Playlist() { _remove_all(); }

		/**
		 * Update playlist
		 */
		void update(Genode::Xml_node const &node)
		{
			using namespace Genode;

			/* handle tracks */
			_remove_all();
			_curr_track = nullptr;

			unsigned count = 0;
			auto add_track = [&] (Xml_node const &track) {
				try {
					bool done = false;

					auto try_location = [&] (Xml_node const &node) {
						if (done) return;

						typedef String<256> Location;
						auto location = node.decoded_content<Location>();
						char const *path = location.string();

						auto _exists_file = [] (char const *path)
						{
							bool exists = false;
							Libc::with_libc([&] () {
								FILE *f = fopen(path, "r");
								if (f != NULL) {
									fclose(f);
									exists = true;
								}
							});
							return exists;
						};

						if (strcmp(path, "file://", 7) == 0) {
							path += 7;
							if (_exists_file(path)) {
								_insert(path, ++count);
								done = true;
							}
						} else
						if (_exists_file(path)) {
							_insert(path, ++count);
							done = true;
						}
					};

					track.for_each_sub_node("location", try_location);

				} catch (...) { Genode::warning("invalid track node in playlist"); }
			};

			try {
				Xml_node const track_list = node.sub_node("trackList");
				track_list.for_each_sub_node("track", add_track);
			} catch (...) { }

			/* handle playlist mode */
			_mode = MODE_ONCE;
			try {
				auto mode = node.attribute_value("mode", String<16>());
				if (mode == "repeat") _mode = MODE_REPEAT;
			} catch (...) { }

			Genode::log("update playlist: ", count,
			            " tracks mode: ", _mode == MODE_ONCE ? "once" : "repeat");
		}

		/**
		 * Return track with given id
		 */
		Track get_track(unsigned id)
		{
			for (Track *t = _track_list.first(); t; t = t->next())
				if (t->id == id) {
					_curr_track = t;
					return *_curr_track;
				}

			return Track();
		}

		/**
		 * Return next track from playlist
		 */
		Track next_track()
		{
			_curr_track = _curr_track ? _curr_track->next()
			                          : _track_list.first();

			/* try to loop at the end of the playlist */
			if (_curr_track == nullptr && _mode == MODE_REPEAT) {
					_curr_track = _track_list.first();
			}

			return _curr_track ? *_curr_track : Track();
		}
};


class Audio_player::Decoder
{
	public:

		/*
		 * Thrown as exception if the decoder could not be initialized
		 */
		struct Not_initialized { };

		/*
		 * File_info contains metadata of the file that is currently decoded
		 */
		struct File_info : public Playlist::Track
		{
			typedef Genode::String<256> Artist;
			typedef Genode::String<256> Album;
			typedef Genode::String<256> Title;
			typedef Genode::String<16>  Track;

			Artist  artist;
			Album   album;
			Title   title;
			Track   track;
			int64_t duration;

			File_info(Playlist::Track const &playlist_track,
			           char const *artist, char const *album,
			           char const *title, char const *track,
			           int64_t duration)
			:
				Playlist::Track(playlist_track),
				artist(artist), album(album), title(title), track(track),
				duration(duration) { }
		};

	private:

		AVFrame         *_frame      = nullptr;
		AVFrame         *_conv_frame = nullptr;
		AVStream        *_stream     = nullptr;
		AVFormatContext *_format_ctx = nullptr;
		AVCodecContext  *_codec_ctx  = nullptr;
		AVPacket         _packet;

		AVAudioResampleContext *_avr = nullptr;

		unsigned _samples_decoded = 0;

		Playlist::Track const &_track;

		Genode::Constructible<File_info> _track_info;

		void _close()
		{
			Libc::with_libc([&] () {
				avformat_close_input(&_format_ctx);
				av_free(_conv_frame);
				av_free(_frame);
			}); /* with_libc */
		}

		/**
		 * Initialize libav once before it gets used
		 */
		static void init()
		{
			static bool registered = false;
			if (registered) { return; }

			Libc::with_libc([&] () {
				/* initialise libav first so that all decoders are present */
				av_register_all();

				/* make libav quiet so we do not need stderr access */
				av_log_set_level(AV_LOG_QUIET);
			}); /* with_libc */

			registered = true;
		}

	public:

		/*
		 * Constructor
		 */
		Decoder(Playlist::Track const &playlist_track)
		: _track(playlist_track)
		{
			bool initialized = false;

			Libc::with_libc([&] () {

				Decoder::init();

				_frame = av_frame_alloc();
				if (!_frame) { return; }
				
				_conv_frame = av_frame_alloc();
				if (!_conv_frame) {
					av_free(_frame);
					return;
				}

				int err = 0;
				err = avformat_open_input(&_format_ctx, _track.path.string(), NULL, NULL);
				if (err != 0) {
					Genode::error("could not open '", _track.path.string(), "'");
					av_free(_conv_frame);
					av_free(_frame);
					return;
				}

				err = avformat_find_stream_info(_format_ctx, NULL);
				if (err < 0) {
					Genode::error("could not find the stream info");
					_close();
					return;
				}

				for (unsigned i = 0; i < _format_ctx->nb_streams; ++i)
					if (_format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
						_stream = _format_ctx->streams[i];
						break;
					}

				if (_stream == nullptr) {
					Genode::error("could not find any audio stream");
					_close();
					return;
				}

				_codec_ctx        = _stream->codec;
				_codec_ctx->codec = avcodec_find_decoder(_codec_ctx->codec_id);
				if (_codec_ctx->codec == NULL) {
					Genode::error("could not find decoder");
					_close();
					return;
				}

				err = avcodec_open2(_codec_ctx, _codec_ctx->codec, NULL);
				if (err != 0) {
					Genode::error("could not open decoder");
					_close();
					return;
				}

				_avr = avresample_alloc_context();
				if (!_avr) {
					_close();
					return;
				}

				av_opt_set_int(_avr, "in_channel_layout",  AV_CH_LAYOUT_STEREO,     0);
				av_opt_set_int(_avr, "out_channel_layout", AV_CH_LAYOUT_STEREO,     0);
				av_opt_set_int(_avr, "in_sample_rate",     _codec_ctx->sample_rate, 0);
				av_opt_set_int(_avr, "out_sample_rate",    Audio_out::SAMPLE_RATE,  0);
				av_opt_set_int(_avr, "in_sample_fmt",      _codec_ctx->sample_fmt,  0);
				av_opt_set_int(_avr, "out_sample_fmt",     AV_SAMPLE_FMT_FLT,       0);

				if (avresample_open(_avr) < 0) {
					_close();
					return;
				}

				_conv_frame->channel_layout = AV_CH_LAYOUT_STEREO;
				_conv_frame->sample_rate    = Audio_out::SAMPLE_RATE;
				_conv_frame->format         = AV_SAMPLE_FMT_FLT;

				av_init_packet(&_packet);

				/* extract metainformation */
				bool const is_vorbis = _codec_ctx->codec_id == AV_CODEC_ID_VORBIS;

				AVDictionary *md = is_vorbis ? _stream->metadata : _format_ctx->metadata;
				int const flags  = AV_DICT_IGNORE_SUFFIX;

				AVDictionaryEntry *artist = av_dict_get(md, "artist", NULL, flags);
				AVDictionaryEntry *album  = av_dict_get(md, "album", NULL, flags);
				AVDictionaryEntry *title  = av_dict_get(md, "title", NULL, flags);
				AVDictionaryEntry *track  = av_dict_get(md, "track", NULL, flags);

				_track_info.construct(_track,
				                      artist ? artist->value : "",
				                      album  ? album->value  : "",
				                      title  ? title->value  : "",
				                      track  ? track->value  : "",
				                      _format_ctx->duration / 1000);

				initialized = true;
			}); /* with_libc */

			if (!initialized) { throw Not_initialized(); }
		}

		/**
		 * Destructor
		 */
		~Decoder()
		{
			Libc::with_libc([&] () {
				avresample_close(_avr);
				avresample_free(&_avr);
				avcodec_close(_codec_ctx);
				avformat_close_input(&_format_ctx);
				av_free(_conv_frame);
				av_free(_frame);
			}); /* with_libc */
		}

		/**
		 * Dump format information - needs <libc stderr="/dev/log"> configuration
		 */
		void dump_info() const
		{
			Libc::with_libc([&] () {
				av_dump_format(_format_ctx, 0, _track.path.string(), 0);
			}); /* with_libc */
		}

		/**
		 * Return metainformation of the file
		 */
		File_info const & file_info() const { return *_track_info; }

		/**
		 * Return decoding progress in ms
		 */
		uint64_t progress() const
		{
			/*
			 * Until there is need for seeking support that is enough albeit
			 * inaccurate and do not get me started on the float abuse...
			 *
			 * XXX We could also count the Audio_out packets submitted per
			 * track to get a more accurate playback progress
			 * (see Out::packets_submitted()).
			 */
			return (float)_samples_decoded / _codec_ctx->sample_rate * 1000;
		}

		/**
		 * Fill frame data buffer with decoded frames
		 *
		 * \param frame_data reference to destination buffer
		 * \param min minimal number of bytes that have to be decoded at least
		 */
		int fill_buffer(Genode::Env &env, Frame_data &frame_data, size_t min)
		{
			size_t written = 0;

			Libc::with_libc([&] {
				while (written < min) {

					if (av_read_frame(_format_ctx, &_packet) != 0) { break; }

					if (_packet.stream_index == _stream->index) {
						int finished = 0;
						avcodec_decode_audio4(_codec_ctx, _frame, &finished, &_packet);

						if (finished) {

							/*
							 * We have to read all available samples, otherwise we
							 * end up leaking memory. Draining all available sample will
							 * lead to distorted audio; checking for > 64 works(tm) but
							 * we might still leak some memory (hopefully the song's duration
							 * is not too long.) FWIW, it seems to be happening only when
							 * resampling vorbis files with FLTP format.
							 */
							AVFrame *in = _frame;
							do {
								if (avresample_convert_frame(_avr, _conv_frame, in) < 0) {
									Genode::error("could not resample frame");
									return 0;
								}

								void   const *data  = _conv_frame->extended_data[0];
								size_t const  bytes = _conv_frame->linesize[0];
								size_t const      s = 2 /* stereo */ * sizeof(float);

								_samples_decoded += bytes / s;
								written += frame_data.write(data, bytes);

								in = nullptr;
							} while (avresample_available(_avr) > 64);
						}
					}

					av_free_packet(&_packet);
				}
				/* keep compiler for complaining b/c of lambda deduction */
				return 0;
			}); /* with_libc */

			return written;
		}
};


struct Audio_player::Main
{
	Genode::Env        &env;

	Genode::Heap        alloc { env.ram(), env.rm() } ;

	void handle_progress();

	Genode::Signal_handler<Main> progress_dispatcher = {
		env.ep(), *this, &Main::handle_progress };

	Output      output { env, progress_dispatcher };
	Frame_data  frame_data;
	Decoder    *decoder = nullptr;

	Playlist        playlist { alloc };
	Playlist::Track track;

	void scan_playlist();

	Genode::Reporter playlist_reporter { env, "playlist" };
	bool report_playlist = false;

	void handle_playlist();

	Genode::Signal_handler<Main> playlist_dispatcher = {
		env.ep(), *this, &Main::handle_playlist };

	Genode::Attached_rom_dataspace playlist_rom { env, "playlist" };

	bool is_paused  = false;
	bool is_stopped = false;

	bool state_changed = false;

	Genode::Reporter reporter { env, "current_track" };

	unsigned report_progress_interval = 0; /* in Audio_out packets */
	bool     report_progress          = false;
	unsigned packet_count             = 0;

	void report_track(Decoder const *d);

	Genode::Attached_rom_dataspace config_rom { env, "config" };

	void handle_config();

	Genode::Signal_handler<Main> config_dispatcher = {
		env.ep(), *this, &Main::handle_config };

	Main(Genode::Env &env) : env(env)
	{
		Genode::Signal_transmitter(config_dispatcher).submit();
		config_rom.sigh(config_dispatcher);

		Genode::Signal_transmitter(playlist_dispatcher).submit();
		playlist_rom.sigh(playlist_dispatcher);

		reporter.enabled(true);
		playlist_reporter.enabled(true);

		Genode::log("--- start Audio_player ---");
	}
};


void Audio_player::Main::scan_playlist()
{
	try {
		Genode::Reporter::Xml_generator xml(playlist_reporter, [&] () {
			xml.attribute("version", 1);
			xml.attribute("xmlns", "http://xspf.org/ns/0/");

			xml.node("trackList", [&] () {
				playlist.for_each_track([&] (Playlist::Track const &t) {
					Decoder d(t);
					Decoder::File_info const &info = d.file_info();
					xml.node("track", [&] () {
						xml.node("location", [&] () {
							xml.append_content(info.path); });
						xml.node("title", [&] () {
							xml.append_content(info.title); });
						xml.node("creator", [&] () {
							xml.append_content(info.artist); });
						xml.node("album", [&] () {
							xml.append_content(info.album); });
						xml.node("duration", [&] () {
							xml.append_content(
								Genode::String<16>(info.duration)); });
					});
				});
			});
		});
	} catch (...) { Genode::warning("could not report playlist"); }
}


void Audio_player::Main::handle_playlist()
{
	playlist_rom.update();

	if (!playlist_rom.valid()) { return; }

	playlist.update(playlist_rom.xml());

	track = playlist.next_track();

	if (report_playlist) { scan_playlist(); }

	/* trigger playback because playlist has changed */
	handle_progress();
}


void Audio_player::Main::report_track(Decoder const *d)
{
	try {
		Genode::Reporter::Xml_generator xml(reporter, [&] () {
			/*
			 * There is no valid decoder, create empty report to notify
			 * agents.
			 */
			if (d == nullptr) { return; }

			Decoder::File_info const &info = d->file_info();
			xml.attribute("id",       info.id);
			xml.attribute("path",     info.path);
			xml.attribute("artist",   info.artist);
			xml.attribute("album",    info.album);
			xml.attribute("title",    info.title);
			xml.attribute("track",    info.track);
			xml.attribute("progress", d->progress());
			xml.attribute("duration", info.duration);

			char const *s = "playing";

			if (is_paused)  { s = "paused"; }
			if (is_stopped) { s = "stopped"; }

			xml.attribute("state", s);
		});
	} catch (...) { Genode::warning("could not report current track"); }
}


void Audio_player::Main::handle_progress()
{
	if (is_stopped) {
		Genode::destroy(&alloc, decoder);
		decoder    = nullptr;
		is_stopped = false;

		report_track(nullptr);
	}

	if (is_paused) { return; }

	/* do not bother, that is not the track you are looking for */
	if (!track.valid()) { return; }

	/* track is valid but there is no decoder yet */
	if (decoder == nullptr) {
		Genode::retry<Decoder::Not_initialized>(
		[&] {
			if (track.valid()) {
				decoder = new (&alloc) Decoder(track);
				report_track(decoder);
				packet_count = 0;
			}
		},
		[&] {
			/* in case it did not work try next track */
			track = playlist.next_track();
		});

		report_track(decoder);
	}

	/* update current track progress */
	if (report_progress
	    && decoder
	    && (++packet_count == report_progress_interval)) {
		report_track(decoder);
		packet_count = 0;
	}

	/* only decode and play if we are below the threshold */
	if (output.queued() < QUEUED_PACKET_THRESHOLD) {

		if (decoder && frame_data.read_avail() <= output.frame_size()) {

			size_t const req_size = output.frame_size();
			int const n = decoder->fill_buffer(env, frame_data, req_size);
			if (n == 0) {
				Genode::destroy(&alloc, decoder);
				decoder = nullptr;

				track = playlist.next_track();
				if (!track.valid()) {
					Genode::warning("reached end of playlist");
					report_track(nullptr);
				}
			}
		}

		if (frame_data.read_avail() >= AUDIO_OUT_PACKET_SIZE)
			output.drain_buffer(frame_data);
	}
}


void Audio_player::Main::handle_config()
{
	config_rom.update();

	if (!config_rom.valid()) { return; }

	Genode::Xml_node config = config_rom.xml();

	/* handle playlist scan request */
	try {
		config.attribute("scan_playlist").has_value("yes");
		scan_playlist();
	} catch (...) { }

	/* handle state */
	bool state_changed = false;
	try {
		static Genode::String<16> last_state { "-" };

		Genode::String<16> state;
		config.attribute("state").value(&state);

		state_changed = state != last_state;

		if (state_changed) {
			if (state == "playing") {
				is_paused = false;
				output.start();
			}
			else if (state == "paused") {
				is_paused = true;
				output.stop();
			}
			else /*(state == "stopped")*/ {
				Genode::error("state == stopped, should not happen");
				is_stopped = true;
				is_paused  = true;
				output.stop();
			}

			last_state = state;
		}

		report_track(decoder);
	} catch (...) {
		/* if there is no state attribute we are stopped */
		Genode::warning("player state invalid, player is stopped");
		is_stopped = true;
		is_paused  = true;
		output.stop();
	}

	/* handle selected track */
	try {
		unsigned id = 0;
		config.attribute("selected_track").value(&id);
		if (id != track.id) {  /* XXX what happens if the playlist changed? */
			is_stopped = true; /* XXX do not abuse this flag */
			track = playlist.get_track(id);

			if (!track.valid()) {
				Genode::error("invalid track ", id, " selected");
			}
		}
	} catch (...) { }

	/* handle progress report */
	enum { DEFAULT_SEC = 5 };
	try {
		Genode::Xml_node node = config.sub_node("report");

		report_progress = node.attribute_value("progress", false);
		unsigned v = node.attribute_value<unsigned>("interval", DEFAULT_SEC);
		report_progress_interval = output.packets_per_sec() * v;

		report_playlist = node.attribute_value("playlist", false);
	} catch (...) {
		report_progress = false;
		report_progress_interval = output.packets_per_sec() * DEFAULT_SEC;
		report_playlist = false;
	}

	/* trigger playback because state might has changed */
	if (state_changed) { handle_progress(); }
}


/***************
 ** Component **
 ***************/

void Libc::Component::construct(Libc::Env &env)
{
	static Audio_player::Main main(env);
}
