// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

#include "PipeWireOutputPlugin.hxx"
#include "lib/pipewire/Error.hxx"
#include "lib/pipewire/ThreadLoop.hxx"
#include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/plugins/PipeWireMixerPlugin.hxx"
#include "pcm/Silence.hxx"
#include "lib/fmt/ExceptionFormatter.hxx"
#include "system/Error.hxx"
#include "util/BitReverse.hxx"
#include "util/Domain.hxx"
#include "util/RingBuffer.hxx"
#include "util/ScopeExit.hxx"
#include "util/StaticVector.hxx"
#include "util/StringCompare.hxx"
#include "Log.hxx"
#include "tag/Format.hxx"
#include "config.h" // for ENABLE_DSD

#ifdef __GNUC__
#pragma GCC diagnostic push
/* oh no, libspa likes to cast away "const"! */
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif

#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>

#include <cmath>

#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

#include <algorithm>
#include <array>
#include <numeric>
#include <stdexcept>
#include <string>

static constexpr Domain pipewire_output_domain("pipewire_output");

class PipeWireOutput final : AudioOutput {
	const char *const name;

	const char *const remote;
	const char *const target;

	struct pw_thread_loop *thread_loop = nullptr;
	struct pw_stream *stream;

	std::string error_message;

	std::byte pod_buffer[1024];
	struct spa_pod_builder pod_builder;

	std::size_t frame_size;

	/**
	 * This buffer passes PCM data from Play() to Process().
	 */
	using RingBuffer = ::RingBuffer<std::byte>;
	RingBuffer ring_buffer;

	uint32_t target_id = PW_ID_ANY;

	/**
	 * The current volume level (0.0 .. 1.0).
	 *
	 * This get initialized to -1 which means "unknown", so
	 * restore_volume will not attempt to override PipeWire's
	 * initial volume level.
	 */
	float volume = -1;

	PipeWireMixer *mixer = nullptr;
	unsigned channels;

	/**
	 * The active sample format, needed for PcmSilence().
	 */
	SampleFormat sample_format;

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	/**
	 * Is the "dsd" setting enabled, i.e. is DSD playback allowed?
	 */
	const bool enable_dsd;

	/**
	 * Are we currently playing in native DSD mode?
	 */
	bool use_dsd;

	/**
	 * Reverse the 8 bits in each DSD byte?  This is necessary if
	 * PipeWire wants LSB (because MPD uses MSB internally).
	 */
	bool dsd_reverse_bits;

	/**
	 * Pack this many bytes of each frame together.  MPD uses 1
	 * internally, and if PipeWire wants more than one
	 * (e.g. because it uses DSD_U32), we need to reorder bytes.
	 */
	uint_least8_t dsd_interleave;
#endif

	bool disconnected;

	/**
	 * Shall the previously known volume be restored as soon as
	 * PW_STREAM_STATE_STREAMING is reached?  This needs to be
	 * done each time after the pw_stream got created, thus this
	 * flag gets set by Open().
	 */
	bool restore_volume;

	bool interrupted;
	bool paused;

	/**
	 * Is the PipeWire stream active, i.e. has
	 * pw_stream_set_active() been called successfully?
	 */
	bool active;

	/**
	 * Has Drain() been called?  This causes Process() to invoke
	 * pw_stream_flush() to drain PipeWire as soon as the
	 * #ring_buffer has been drained.
	 */
	bool drain_requested;

	bool drained;

	explicit PipeWireOutput(const ConfigBlock &block);

public:
	static AudioOutput *Create(EventLoop &,
				   const ConfigBlock &block) {
		pw_init(nullptr, nullptr);

		return new PipeWireOutput(block);
	}

	static constexpr struct pw_stream_events MakeStreamEvents() noexcept {
		struct pw_stream_events events{};
		events.version = PW_VERSION_STREAM_EVENTS;
		events.state_changed = StateChanged;
		events.process = Process;
		events.drained = Drained;
		events.control_info = ControlInfo;
		events.param_changed = ParamChanged;
		return events;
	}

	void SetVolume(float volume);

	void SetMixer(PipeWireMixer &_mixer) noexcept;

	void ClearMixer([[maybe_unused]] PipeWireMixer &old_mixer) noexcept {
		assert(mixer == &old_mixer);

		mixer = nullptr;
	}

private:
	void CheckThrowError() {
		if (disconnected) {
			if (error_message.empty())
				throw std::runtime_error("Disconnected from PipeWire");
			else
				throw std::runtime_error(error_message);
		}
	}

	void StateChanged(enum pw_stream_state state,
			  const char *error) noexcept;

	static void StateChanged(void *data,
				 [[maybe_unused]] enum pw_stream_state old,
				 enum pw_stream_state state,
				 const char *error) noexcept {
		auto &o = *(PipeWireOutput *)data;
		o.StateChanged(state, error);
	}

	void Process() noexcept;

	static void Process(void *data) noexcept {
		auto &o = *(PipeWireOutput *)data;
		o.Process();
	}

	void Drained() noexcept {
		drained = true;
		pw_thread_loop_signal(thread_loop, false);
	}

	static void Drained(void *data) noexcept {
		auto &o = *(PipeWireOutput *)data;
		o.Drained();
	}

	void OnChannelVolumes(const struct pw_stream_control &control) noexcept {
		if (control.n_values < 1)
			return;

		float sum = std::accumulate(control.values,
					    control.values + control.n_values,
					    0.0f);
		volume = std::cbrt(sum / control.n_values);

		if (mixer != nullptr)
			pipewire_mixer_on_change(*mixer, volume);

		pw_thread_loop_signal(thread_loop, false);
	}

	void ControlInfo([[maybe_unused]] uint32_t id,
			 const struct pw_stream_control &control) noexcept {
		switch (id) {
		case SPA_PROP_channelVolumes:
			OnChannelVolumes(control);
			break;
		}
	}

	static void ControlInfo(void *data, uint32_t id,
				const struct pw_stream_control *control) noexcept {
		auto &o = *(PipeWireOutput *)data;
		o.ControlInfo(id, *control);
	}

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	void DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept;
	void DsdFormatChanged(const struct spa_pod &param) noexcept;
#endif

	void ParamChanged(uint32_t id, const struct spa_pod *param) noexcept;

	static void ParamChanged(void *data,
				 uint32_t id,
				 const struct spa_pod *param) noexcept
	{
		if (id != SPA_PARAM_Format || param == nullptr)
			return;

		auto &o = *(PipeWireOutput *)data;
		o.ParamChanged(id, param);
	}

	/* virtual methods from class AudioOutput */
	void Enable() override;
	void Disable() noexcept override;

	void Open(AudioFormat &audio_format) override;
	void Close() noexcept override;

	void Interrupt() noexcept override {
		if (thread_loop == nullptr)
			return;

		const PipeWire::ThreadLoopLock lock(thread_loop);
		interrupted = true;
		pw_thread_loop_signal(thread_loop, false);
	}

	[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
	std::size_t Play(std::span<const std::byte> src) override;

	void Drain() override;
	void Cancel() noexcept override;
	bool Pause() noexcept override;

	void SendTag(const Tag &tag) override;
};

static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents();

inline
PipeWireOutput::PipeWireOutput(const ConfigBlock &block)
	:AudioOutput(FLAG_ENABLE_DISABLE),
	 name(block.GetBlockValue("name", "pipewire")),
	 remote(block.GetBlockValue("remote", nullptr)),
	 target(block.GetBlockValue("target", nullptr))
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	, enable_dsd(block.GetBlockValue("dsd", false))
#endif
{
	if (target != nullptr) {
		if (StringIsEmpty(target))
			throw std::runtime_error("target must not be empty");

		char *endptr;
		const auto _target_id = strtoul(target, &endptr, 10);
		if (endptr > target && *endptr == 0)
			/* numeric value means target_id, not target
			   name */
			target_id = (uint32_t)_target_id;
	}
}

/**
 * Throws on error.
 *
 * @param volume a volume level between 0.0 and 1.0
 */
static void
SetVolume(struct pw_stream &stream, unsigned channels, float volume)
{
	float value[MAX_CHANNELS];
	std::fill_n(value, channels, volume * volume * volume);

	if (pw_stream_set_control(&stream,
				  SPA_PROP_channelVolumes, channels, value,
				  0) != 0)
		throw std::runtime_error("pw_stream_set_control() failed");
}

void
PipeWireOutput::SetVolume(float _volume)
{
	if (thread_loop == nullptr) {
		/* the mixer is open (because it is a "global" mixer),
		   but Enable() on this output has not yet been
		   called */
		volume = _volume;
		return;
	}

	const PipeWire::ThreadLoopLock lock(thread_loop);

	if (stream != nullptr && !restore_volume)
		::SetVolume(*stream, channels, _volume);

	volume = _volume;
}

void
PipeWireOutput::Enable()
{
	thread_loop = pw_thread_loop_new(name, nullptr);
	if (thread_loop == nullptr)
		throw MakeErrno("pw_thread_loop_new() failed");

	pw_thread_loop_start(thread_loop);

	stream = nullptr;
}

void
PipeWireOutput::Disable() noexcept
{
	pw_thread_loop_destroy(thread_loop);
	thread_loop = nullptr;
}

static constexpr enum spa_audio_format
ToPipeWireSampleFormat(SampleFormat format) noexcept
{
	switch (format) {
	case SampleFormat::UNDEFINED:
		break;

	case SampleFormat::S8:
		return SPA_AUDIO_FORMAT_S8;

	case SampleFormat::S16:
		return SPA_AUDIO_FORMAT_S16;

	case SampleFormat::S24_P32:
		return SPA_AUDIO_FORMAT_S24_32;

	case SampleFormat::S32:
		return SPA_AUDIO_FORMAT_S32;

	case SampleFormat::FLOAT:
		return SPA_AUDIO_FORMAT_F32;

	case SampleFormat::DSD:
		break;
	}

	return SPA_AUDIO_FORMAT_UNKNOWN;
}

static struct spa_audio_info_raw
ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept
{
	struct spa_audio_info_raw raw{};

	raw.format = ToPipeWireSampleFormat(audio_format.format);
	if (raw.format == SPA_AUDIO_FORMAT_UNKNOWN) {
		raw.format = SPA_AUDIO_FORMAT_S16;
		audio_format.format = SampleFormat::S16;
	}

	raw.flags = SPA_AUDIO_FLAG_NONE;
	raw.rate = audio_format.sample_rate;
	raw.channels = audio_format.channels;

	/* MPD uses the FLAC channel assignment
	   (https://xiph.org/flac/format.html) */
	switch (audio_format.channels) {
	case 1:
		raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
		break;

	case 2:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		break;

	case 3:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_FC;
		break;

	case 4:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_RL;
		raw.position[3] = SPA_AUDIO_CHANNEL_RR;
		break;

	case 5:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_FC;
		raw.position[3] = SPA_AUDIO_CHANNEL_RL;
		raw.position[4] = SPA_AUDIO_CHANNEL_RR;
		break;

	case 6:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_FC;
		raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
		raw.position[4] = SPA_AUDIO_CHANNEL_RL;
		raw.position[5] = SPA_AUDIO_CHANNEL_RR;
		break;

	case 7:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_FC;
		raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
		raw.position[4] = SPA_AUDIO_CHANNEL_RC;
		raw.position[5] = SPA_AUDIO_CHANNEL_SL;
		raw.position[6] = SPA_AUDIO_CHANNEL_SR;
		break;

	case 8:
		raw.position[0] = SPA_AUDIO_CHANNEL_FL;
		raw.position[1] = SPA_AUDIO_CHANNEL_FR;
		raw.position[2] = SPA_AUDIO_CHANNEL_FC;
		raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
		raw.position[4] = SPA_AUDIO_CHANNEL_RL;
		raw.position[5] = SPA_AUDIO_CHANNEL_RR;
		raw.position[6] = SPA_AUDIO_CHANNEL_SL;
		raw.position[7] = SPA_AUDIO_CHANNEL_SR;
		break;

	default:
		raw.flags |= SPA_AUDIO_FLAG_UNPOSITIONED;
	}

	return raw;
}

void
PipeWireOutput::Open(AudioFormat &audio_format)
{
	error_message.clear();
	disconnected = false;
	restore_volume = true;

	paused = false;

	/* stay inactive (PW_STREAM_FLAG_INACTIVE) until the ring
	   buffer has been filled */
	active = false;

	drain_requested = false;
	drained = true;

	auto props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
				       PW_KEY_MEDIA_CATEGORY, "Playback",
				       PW_KEY_MEDIA_ROLE, "Music",
				       PW_KEY_APP_NAME, "Music Player Daemon",
				       PW_KEY_APP_ICON_NAME, "mpd",
				       nullptr);

	pw_properties_setf(props, PW_KEY_NODE_NAME, "mpd.%s", name);

	if (remote != nullptr && target_id == PW_ID_ANY)
		pw_properties_setf(props, PW_KEY_REMOTE_NAME, "%s", remote);

	if (target != nullptr && target_id == PW_ID_ANY)
		pw_properties_setf(props,
#if PW_CHECK_VERSION(0, 3, 64)
				   PW_KEY_TARGET_OBJECT,
#else
				   PW_KEY_NODE_TARGET,
#endif
				   "%s", target);

#ifdef PW_KEY_NODE_RATE
	/* ask PipeWire to change the graph sample rate to ours
	   (requires PipeWire 0.3.32) */
	pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u",
			   audio_format.sample_rate);
#endif

	const PipeWire::ThreadLoopLock lock(thread_loop);

	stream = pw_stream_new_simple(pw_thread_loop_get_loop(thread_loop),
				      "mpd",
				      props,
				      &stream_events,
				      this);
	if (stream == nullptr)
		throw MakeErrno("pw_stream_new_simple() failed");

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	/* this needs to be determined before ToPipeWireAudioFormat()
	   switches DSD to S16 */
	use_dsd = enable_dsd &&
		audio_format.format == SampleFormat::DSD;
	dsd_reverse_bits = false;
	dsd_interleave = 0;
#endif

	auto raw = ToPipeWireAudioFormat(audio_format);

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	if (use_dsd)
		/* restore the DSD format which was overwritten by
		   ToPipeWireAudioFormat(), because DSD is a special
		   case in PipeWire */
		audio_format.format = SampleFormat::DSD;
#endif

	frame_size = audio_format.GetFrameSize();
	sample_format = audio_format.format;
	channels = audio_format.channels;
	interrupted = false;

	/* allocate a ring buffer of 0.5 seconds */
	ring_buffer = RingBuffer{frame_size * (audio_format.sample_rate / 2)};

	const struct spa_pod *params[1];

	pod_builder = {};
	pod_builder.data = pod_buffer;
	pod_builder.size = sizeof(pod_buffer);

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	struct spa_audio_info_dsd dsd;
	if (use_dsd) {
		dsd = {};

		/* copy all relevant settings from the
		   ToPipeWireAudioFormat() return value */
		dsd.flags = raw.flags;
		dsd.rate = raw.rate;
		dsd.channels = raw.channels;
		if ((dsd.flags & SPA_AUDIO_FLAG_UNPOSITIONED) == 0)
			std::copy_n(raw.position, dsd.channels, dsd.position);

		params[0] = spa_format_audio_dsd_build(&pod_builder,
						       SPA_PARAM_EnumFormat,
						       &dsd);
	} else
#endif
		params[0] = spa_format_audio_raw_build(&pod_builder,
						       SPA_PARAM_EnumFormat,
						       &raw);

	int error =
		pw_stream_connect(stream,
				  PW_DIRECTION_OUTPUT,
				  target_id,
				  (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT |
							 PW_STREAM_FLAG_INACTIVE |
							 PW_STREAM_FLAG_MAP_BUFFERS |
							 PW_STREAM_FLAG_RT_PROCESS),
				  params, 1);
	if (error < 0)
		throw PipeWire::MakeError(error, "Failed to connect stream");
}

void
PipeWireOutput::Close() noexcept
{
	{
		const PipeWire::ThreadLoopLock lock(thread_loop);
		pw_stream_destroy(stream);
		stream = nullptr;
	}

	ring_buffer = {};
}

inline void
PipeWireOutput::StateChanged(enum pw_stream_state state,
			     const char *error) noexcept
{
	const bool was_disconnected = disconnected;
	disconnected = state == PW_STREAM_STATE_ERROR ||
		state == PW_STREAM_STATE_UNCONNECTED;
	if (!was_disconnected && disconnected) {
		if (error != nullptr)
			error_message = error;

		pw_thread_loop_signal(thread_loop, false);
	}

}

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)

inline void
PipeWireOutput::DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept
{
	/* MPD uses MSB internally, which means if PipeWire asks LSB
	   from us, we need to reverse the bits in each DSD byte */
	dsd_reverse_bits = dsd.bitorder == SPA_PARAM_BITORDER_lsb;

	dsd_interleave = dsd.interleave;
}

inline void
PipeWireOutput::DsdFormatChanged(const struct spa_pod &param) noexcept
{
	uint32_t media_type, media_subtype;
	struct spa_audio_info_dsd dsd;

	if (spa_format_parse(&param, &media_type, &media_subtype) >= 0 &&
	    media_type == SPA_MEDIA_TYPE_audio &&
	    media_subtype == SPA_MEDIA_SUBTYPE_dsd &&
	    spa_format_audio_dsd_parse(&param, &dsd) >= 0)
		DsdFormatChanged(dsd);
}

#endif

inline void
PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id,
			     [[maybe_unused]] const struct spa_pod *param) noexcept
{
	if (restore_volume) {
		restore_volume = false;

		if (volume >= 0) {
			try {
				::SetVolume(*stream, channels, volume);
			} catch (...) {
				FmtError(pipewire_output_domain,
					 "Failed to restore volume: {}",
					 std::current_exception());
			}
		}
	}

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	if (use_dsd && id == SPA_PARAM_Format && param != nullptr)
		DsdFormatChanged(*param);
#endif
}

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)

static void
Interleave(std::byte *data, std::byte *end,
	   std::size_t channels, std::size_t interleave) noexcept
{
	assert(channels > 1);
	assert(channels <= MAX_CHANNELS);

	constexpr std::size_t MAX_INTERLEAVE = 8;
	assert(interleave > 1);
	assert(interleave <= MAX_INTERLEAVE);

	std::array<std::byte, MAX_CHANNELS * MAX_INTERLEAVE> buffer;
	std::size_t buffer_size = channels * interleave;

	while (data < end) {
		std::copy_n(data, buffer_size, buffer.data());

		const std::byte *src0 = buffer.data();
		for (std::size_t channel = 0; channel < channels;
		     ++channel, ++src0) {
			const std::byte *src = src0;
			for (std::size_t i = 0; i < interleave;
			     ++i, src += channels)
				*data++ = *src;
		}
	}
}

static void
BitReverse(std::byte *data, std::size_t n) noexcept
{
	while (n-- > 0)
		*data = BitReverse(*data);
}

static void
PostProcessDsd(std::byte *data, struct spa_chunk &chunk, unsigned channels,
	       bool reverse_bits, unsigned interleave) noexcept
{
	assert(chunk.size % channels == 0);

	if (interleave > 1 && channels > 1) {
		assert(chunk.size % (channels * interleave) == 0);

		Interleave(data, data + chunk.size, channels, interleave);
		chunk.stride *= interleave;
	}

	if (reverse_bits)
		BitReverse(data, chunk.size);
}

#endif

inline void
PipeWireOutput::Process() noexcept
{
	if (drain_requested && ring_buffer.IsEmptyRelaxed()) {
		/* draining was requested and our ring buffer is
		   empty: tell PipeWire to drain (instead of queueing
		   a buffer) */
		pw_stream_flush(stream, true);
		return;
	}

	auto *b = pw_stream_dequeue_buffer(stream);
	if (b == nullptr) {
		pw_log_warn("out of buffers: %m");
		return;
	}

	auto &buffer = *b->buffer;
	auto &d = buffer.datas[0];

	const std::span<std::byte> dest{reinterpret_cast<std::byte *>(d.data), d.maxsize};
	if (dest.data() == nullptr)
		return;

	std::size_t chunk_size = frame_size;

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	if (use_dsd && dsd_interleave > 1) {
		/* make sure we don't get partial interleave frames */
		chunk_size *= dsd_interleave;
	}
#endif

	size_t nbytes = ring_buffer.ReadFramesTo(dest, chunk_size);
	assert(nbytes % chunk_size == 0);
	if (nbytes == 0) {
		/* buffer underrun: generate some silence */
		std::size_t max_chunks = d.maxsize / chunk_size;
		nbytes = max_chunks * chunk_size;
		PcmSilence(dest.first(nbytes), sample_format);

		LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
	}

	auto &chunk = *d.chunk;
	chunk.offset = 0;
	chunk.stride = frame_size;
	chunk.size = nbytes;

#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
	if (use_dsd)
		PostProcessDsd(dest.data(), chunk, channels,
			       dsd_reverse_bits, dsd_interleave);
#endif

	pw_stream_queue_buffer(stream, b);

	pw_thread_loop_signal(thread_loop, false);
}

std::chrono::steady_clock::duration
PipeWireOutput::Delay() const noexcept
{
	const PipeWire::ThreadLoopLock lock(thread_loop);

	auto result = std::chrono::steady_clock::duration::zero();
	if (paused)
		/* idle while paused */
		result = std::chrono::seconds(1);

	return result;
}

std::size_t
PipeWireOutput::Play(std::span<const std::byte> src)
{
	const PipeWire::ThreadLoopLock lock(thread_loop);

	paused = false;

	while (true) {
		CheckThrowError();

		std::size_t bytes_written =
			ring_buffer.WriteFrom(src);
		if (bytes_written > 0) {
			drained = false;
			return bytes_written;
		}

		if (!active) {
			/* now that the ring_buffer is full, there is
			   enough data for Process(), so let's resume
			   the stream now */
			active = true;
			pw_stream_set_active(stream, true);
		}

		if (interrupted)
			throw AudioOutputInterrupted{};

		pw_thread_loop_wait(thread_loop);
	}
}

void
PipeWireOutput::Drain()
{
	const PipeWire::ThreadLoopLock lock(thread_loop);

	if (drained)
		return;

	if (!active) {
		/* there is data in the ring_buffer, but the stream is
		   not yet active; activate it now to ensure it is
		   played before this method returns */
		active = true;
		pw_stream_set_active(stream, true);
	}

	drain_requested = true;
	AtScopeExit(this) { drain_requested = false; };

	while (!drained && !interrupted) {
		CheckThrowError();
		pw_thread_loop_wait(thread_loop);
	}
}

void
PipeWireOutput::Cancel() noexcept
{
	const PipeWire::ThreadLoopLock lock(thread_loop);
	interrupted = false;

	if (drained)
		return;

	/* clear MPD's ring buffer */
	ring_buffer.Clear();

	/* clear libpipewire's buffer */
	pw_stream_flush(stream, false);
	drained = true;

	/* pause the PipeWire stream so libpipewire ceases invoking
	   the "process" callback (we have no data until our Play()
	   method gets called again); the stream will be resume by
	   Play() after the ring_buffer has been refilled */
	if (active) {
		active = false;
		pw_stream_set_active(stream, false);
	}
}

bool
PipeWireOutput::Pause() noexcept
{
	const PipeWire::ThreadLoopLock lock(thread_loop);
	interrupted = false;

	paused = true;

	if (active) {
		active = false;
		pw_stream_set_active(stream, false);
	}

	return true;
}

inline void
PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
{
	assert(mixer == nullptr);

	mixer = &_mixer;

	// TODO: Check if context and stream is ready and trigger a volume update...
}

void
PipeWireOutput::SendTag(const Tag &tag)
{
	CheckThrowError();

	static constexpr struct {
		TagType mpd;
		const char *pipewire;
	} tag_map[] = {
		{ TAG_ARTIST, PW_KEY_MEDIA_ARTIST },
		{ TAG_TITLE, PW_KEY_MEDIA_TITLE },
		{ TAG_DATE, PW_KEY_MEDIA_DATE },
		{ TAG_COMMENT, PW_KEY_MEDIA_COMMENT },
	};

	StaticVector<spa_dict_item, 1 + std::size(tag_map)> items;

	char *medianame = FormatTag(tag, "%artist% - %title%");
	AtScopeExit(medianame) { free(medianame); };

	items.push_back(SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, medianame));

	for (const auto &i : tag_map)
		if (const char *value = tag.GetValue(i.mpd))
			items.push_back(SPA_DICT_ITEM_INIT(i.pipewire, value));

	struct spa_dict dict = SPA_DICT_INIT(items.data(), (uint32_t)items.size());

	const PipeWire::ThreadLoopLock lock(thread_loop);

	auto rc = pw_stream_update_properties(stream, &dict);
	if (rc < 0)
		LogWarning(pipewire_output_domain, "Error updating properties");
}

void
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
	po.SetMixer(pm);
}

void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
	po.ClearMixer(pm);
}

const struct AudioOutputPlugin pipewire_output_plugin = {
	"pipewire",
	nullptr,
	&PipeWireOutput::Create,
	&pipewire_mixer_plugin,
};

void
pipewire_output_set_volume(PipeWireOutput &output, float volume)
{
	output.SetVolume(volume);
}
