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

#include "config.h"
#include "OSXOutputPlugin.hxx"
#include "apple/AudioObject.hxx"
#include "apple/AudioUnit.hxx"
#include "apple/StringRef.hxx"
#include "apple/Throw.hxx"
#include "../OutputAPI.hxx"
#include "mixer/plugins/OSXMixerPlugin.hxx"
#include "lib/fmt/RuntimeError.hxx"
#include "lib/fmt/ToBuffer.hxx"
#include "util/Domain.hxx"
#include "util/Manual.hxx"
#include "pcm/Export.hxx"
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
#include "util/ByteOrder.hxx"
#include "util/CharUtil.hxx"
#include "util/RingBuffer.hxx"
#include "util/StringAPI.hxx"
#include "util/StringBuffer.hxx"
#include "Log.hxx"

#include <CoreAudio/CoreAudio.h>
#include <AudioUnit/AudioUnit.h>
#include <AudioToolbox/AudioToolbox.h>
#include <CoreServices/CoreServices.h>

#include <memory>
#include <span>

// Backward compatibility from OSX 12.0 API change
#if (__MAC_OS_X_VERSION_MAX_ALLOWED >= 120000)
	#define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMain
	#define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMainVolume
#else
	#define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMaster
	#define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMasterVolume
#endif

static constexpr unsigned MPD_OSX_BUFFER_TIME_MS = 100;

static auto
StreamDescriptionToString(const AudioStreamBasicDescription desc) noexcept
{
	// Only convert the lpcm formats (nothing else supported / used by MPD)
	assert(desc.mFormatID == kAudioFormatLinearPCM);

	return FmtBuffer<256>("{} channel {} {}interleaved {}-bit {} {} ({}Hz)",
			      desc.mChannelsPerFrame,
			      (desc.mFormatFlags & kAudioFormatFlagIsNonMixable) ? "" : "mixable",
			      (desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? "non-" : "",
			      desc.mBitsPerChannel,
			      (desc.mFormatFlags & kAudioFormatFlagIsFloat) ? "Float" : "SInt",
			      (desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE",
			      desc.mSampleRate);
}


struct OSXOutput final : AudioOutput {
	/* configuration settings */
	OSType component_subtype;
	/* only applicable with kAudioUnitSubType_HALOutput */
	const char *device_name;
	const char *const channel_map;
	const bool hog_device;

	bool pause;

	/**
	 * Is the audio unit "started", i.e. was AudioOutputUnitStart() called?
	 */
	bool started;

#ifdef ENABLE_DSD
	/**
	 * Enable DSD over PCM according to the DoP standard?
	 *
	 * @see http://dsd-guide.com/dop-open-standard
	 */
	const bool dop_setting;
	bool dop_enabled;
	Manual<PcmExport> pcm_export;
#endif

	AudioDeviceID dev_id;
	AudioComponentInstance au;
	AudioStreamBasicDescription asbd;

	using RingBuffer = ::RingBuffer<std::byte>;
	RingBuffer ring_buffer;

	OSXOutput(const ConfigBlock &block);

	static AudioOutput *Create(EventLoop &, const ConfigBlock &block);
	int GetVolume();
	void SetVolume(unsigned new_volume);

private:
	void Enable() override;
	void Disable() noexcept override;

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

	std::chrono::steady_clock::duration Delay() const noexcept override;
	std::size_t Play(std::span<const std::byte> src) override;
	bool Pause() override;
	void Cancel() noexcept override;
};

static constexpr Domain osx_output_domain("osx_output");

static bool
osx_output_test_default_device()
{
	/* on a Mac, this is always the default plugin, if nothing
	   else is configured */
	return true;
}

OSXOutput::OSXOutput(const ConfigBlock &block)
	:AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE),
	 channel_map(block.GetBlockValue("channel_map")),
	 hog_device(block.GetBlockValue("hog_device", false))
#ifdef ENABLE_DSD
	, dop_setting(block.GetBlockValue("dop", false))
#endif
{
	const char *device = block.GetBlockValue("device");

	if (device == nullptr || StringIsEqual(device, "default")) {
		component_subtype = kAudioUnitSubType_DefaultOutput;
		device_name = nullptr;
	}
	else if (StringIsEqual(device, "system")) {
		component_subtype = kAudioUnitSubType_SystemOutput;
		device_name = nullptr;
	}
	else {
		component_subtype = kAudioUnitSubType_HALOutput;
		/* XXX am I supposed to strdup() this? */
		device_name = device;
	}
}

AudioOutput *
OSXOutput::Create(EventLoop &, const ConfigBlock &block)
{
	OSXOutput *oo = new OSXOutput(block);

	static constexpr AudioObjectPropertyAddress default_system_output_device{
		kAudioHardwarePropertyDefaultSystemOutputDevice,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
	};

	static constexpr AudioObjectPropertyAddress default_output_device{
		kAudioHardwarePropertyDefaultOutputDevice,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	const auto &aopa =
		oo->component_subtype == kAudioUnitSubType_SystemOutput
		// get system output dev_id if configured
		? default_system_output_device
		/* fallback to default device initially (can still be
		   changed by osx_output_set_device) */
		: default_output_device;

	AudioDeviceID dev_id = kAudioDeviceUnknown;
	UInt32 dev_id_size = sizeof(dev_id);
	AudioObjectGetPropertyData(kAudioObjectSystemObject,
				   &aopa,
				   0,
				   NULL,
				   &dev_id_size,
				   &dev_id);
	oo->dev_id = dev_id;

	return oo;
}


int
OSXOutput::GetVolume()
{
	static constexpr AudioObjectPropertyAddress aopa = {
		KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
	};

	const auto vol = AudioObjectGetPropertyDataT<Float32>(dev_id,
							      aopa);

	return static_cast<int>(vol * 100.0f);
}

void
OSXOutput::SetVolume(unsigned new_volume)
{
	Float32 vol = new_volume / 100.0;
	static constexpr AudioObjectPropertyAddress aopa = {
		KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};
	UInt32 size = sizeof(vol);
	OSStatus status = AudioObjectSetPropertyData(dev_id,
						     &aopa,
						     0,
						     NULL,
						     size,
						     &vol);

	if (status != noErr)
		Apple::ThrowOSStatus(status);
}

static void
osx_output_parse_channel_map(const char *device_name,
			     const char *channel_map_str,
			     SInt32 channel_map[],
			     UInt32 num_channels)
{
	unsigned int inserted_channels = 0;
	bool want_number = true;

	while (*channel_map_str) {
		if (inserted_channels >= num_channels)
			throw FmtRuntimeError("{}: channel map contains more than {} entries or trailing garbage",
					      device_name, num_channels);

		if (!want_number && *channel_map_str == ',') {
			++channel_map_str;
			want_number = true;
			continue;
		}

		if (want_number &&
			(IsDigitASCII(*channel_map_str) || *channel_map_str == '-')
		) {
			char *endptr;
			channel_map[inserted_channels] = strtol(channel_map_str, &endptr, 10);
			if (channel_map[inserted_channels] < -1)
				throw FmtRuntimeError("{}: channel map value {} not allowed (must be -1 or greater)",
						      device_name, channel_map[inserted_channels]);

			channel_map_str = endptr;
			want_number = false;
			FmtDebug(osx_output_domain,
				 "{}: channel_map[{}] = {}",
				 device_name, inserted_channels,
				 channel_map[inserted_channels]);
			++inserted_channels;
			continue;
		}

		throw FmtRuntimeError("{}: invalid character {:?} in channel map",
				      device_name, *channel_map_str);
	}

	if (inserted_channels < num_channels)
		throw FmtRuntimeError("{}: channel map contains less than {} entries",
				      device_name, num_channels);
}

static UInt32
AudioUnitGetChannelsPerFrame(AudioUnit inUnit)
{
	const auto desc = AudioUnitGetPropertyT<AudioStreamBasicDescription>(inUnit,
									     kAudioUnitProperty_StreamFormat,
									     kAudioUnitScope_Output,
									     0);
	return desc.mChannelsPerFrame;
}

static void
osx_output_set_channel_map(OSXOutput *oo)
{
	OSStatus status;

	const UInt32 num_channels = AudioUnitGetChannelsPerFrame(oo->au);
	auto channel_map = std::make_unique<SInt32[]>(num_channels);
	osx_output_parse_channel_map(oo->device_name,
				     oo->channel_map,
				     channel_map.get(),
				     num_channels);

	UInt32 size = num_channels * sizeof(SInt32);
	status = AudioUnitSetProperty(oo->au,
		kAudioOutputUnitProperty_ChannelMap,
		kAudioUnitScope_Input,
		0,
		channel_map.get(),
		size);
	if (status != noErr)
		Apple::ThrowOSStatus(status, "unable to set channel map");
}


static float
osx_output_score_sample_rate(Float64 destination_rate, unsigned source_rate)
{
	float score = 0;
	double int_portion;
	double frac_portion = modf(source_rate / destination_rate, &int_portion);
	// prefer sample rates that are multiples of the source sample rate
	if (frac_portion < 0.01 || frac_portion >= 0.99)
		score += 1000;
	// prefer exact matches over other multiples
	score += (int_portion == 1.0) ? 500 : 0;
	if (source_rate == destination_rate)
		score += 1000;
	else if (source_rate > destination_rate)
		score += (int_portion > 1 && int_portion < 100) ? (100 - int_portion) / 100 * 100 : 0;
	else
		score += (int_portion > 1 && int_portion < 100) ? (100 + int_portion) / 100 * 100 : 0;

	return score;
}

static float
osx_output_score_format(const AudioStreamBasicDescription &format_desc,
			const AudioStreamBasicDescription &target_format)
{
	float score = 0;
	// Score only linear PCM formats (everything else MPD cannot use)
	if (format_desc.mFormatID == kAudioFormatLinearPCM) {
		score += osx_output_score_sample_rate(format_desc.mSampleRate,
						      target_format.mSampleRate);

		// Just choose the stream / format with the highest number of output channels
		score += format_desc.mChannelsPerFrame * 5;

		if (target_format.mFormatFlags == kLinearPCMFormatFlagIsFloat) {
			// for float, prefer the highest bitdepth we have
			if (format_desc.mBitsPerChannel >= 16)
				score += (format_desc.mBitsPerChannel / 8);
		} else {
			if (format_desc.mBitsPerChannel == target_format.mBitsPerChannel)
				score += 5;
			else if (format_desc.mBitsPerChannel > target_format.mBitsPerChannel)
				score += 1;

		}
	}

	return score;
}

static Float64
osx_output_set_device_format(AudioDeviceID dev_id,
			     const AudioStreamBasicDescription &target_format)
{
	static constexpr AudioObjectPropertyAddress aopa_device_streams = {
		kAudioDevicePropertyStreams,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	static constexpr AudioObjectPropertyAddress aopa_stream_direction = {
		kAudioStreamPropertyDirection,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	static constexpr AudioObjectPropertyAddress aopa_stream_phys_formats = {
		kAudioStreamPropertyAvailablePhysicalFormats,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	static constexpr AudioObjectPropertyAddress aopa_stream_phys_format = {
		kAudioStreamPropertyPhysicalFormat,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	OSStatus err;

	const auto streams =
		AudioObjectGetPropertyDataArray<AudioStreamID>(dev_id,
							       aopa_device_streams);

	bool format_found = false;
	int output_stream;
	AudioStreamBasicDescription output_format;

	for (const auto stream : streams) {
		const auto direction =
			AudioObjectGetPropertyDataT<UInt32>(stream,
							    aopa_stream_direction);
		if (direction != 0)
			continue;

		const auto format_list =
			AudioObjectGetPropertyDataArray<AudioStreamRangedDescription>(stream,
										      aopa_stream_phys_formats);

		float output_score = 0;

		for (const auto &format : format_list) {
			AudioStreamBasicDescription format_desc = format.mFormat;
			std::string format_string;

			// for devices with kAudioStreamAnyRate
			// we use the requested samplerate here
			if (format_desc.mSampleRate == kAudioStreamAnyRate)
				format_desc.mSampleRate = target_format.mSampleRate;
			float score = osx_output_score_format(format_desc, target_format);

			// print all (linear pcm) formats and their rating
			if (score > 0.0f)
				FmtDebug(osx_output_domain,
					 "Format: {} rated {}",
					 StreamDescriptionToString(format_desc).c_str(),
					 score);

			if (score > output_score) {
				output_score  = score;
				output_format = format_desc;
				output_stream = stream; // set the idx of the stream in the device
				format_found = true;
			}
		}
	}

	if (format_found) {
		err = AudioObjectSetPropertyData(output_stream,
						 &aopa_stream_phys_format,
						 0,
						 NULL,
						 sizeof(output_format),
						 &output_format);
		if (err != noErr)
			throw FmtRuntimeError("Failed to change the stream format: {}",
					      err);
	}

	return output_format.mSampleRate;
}

static UInt32
osx_output_set_buffer_size(AudioUnit au, AudioStreamBasicDescription desc)
{
	const auto value_range = AudioUnitGetPropertyT<AudioValueRange>(au,
									kAudioDevicePropertyBufferFrameSizeRange,
									kAudioUnitScope_Global,
									0);

	try {
		AudioUnitSetBufferFrameSize(au, value_range.mMaximum);
	} catch (...) {
		LogError(std::current_exception(),
			 "Failed to set maximum buffer size");
	}

	auto buffer_frame_size = AudioUnitGetBufferFrameSize(au);
	buffer_frame_size *= desc.mBytesPerFrame;

	// We set the frame size to a power of two integer that
	// is larger than buffer_frame_size.
	UInt32 frame_size = 1;
	while (frame_size < buffer_frame_size + 1)
		frame_size <<= 1;

	return frame_size;
}

static void
osx_output_hog_device(AudioDeviceID dev_id, bool hog) noexcept
{
	static constexpr AudioObjectPropertyAddress aopa = {
		kAudioDevicePropertyHogMode,
		kAudioObjectPropertyScopeOutput,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
	};

	pid_t hog_pid;

	try {
		hog_pid = AudioObjectGetPropertyDataT<pid_t>(dev_id, aopa);
	} catch (...) {
		Log(LogLevel::DEBUG, std::current_exception(),
		    "Failed to query HogMode");
		return;
	}

	if (hog) {
		if (hog_pid != -1) {
			LogDebug(osx_output_domain,
				 "Device is already hogged");
			return;
		}
	} else {
		if (hog_pid != getpid()) {
			FmtDebug(osx_output_domain,
				 "Device is not owned by this process");
			return;
		}
	}

	hog_pid = hog ? getpid() : -1;
	UInt32 size = sizeof(hog_pid);
	OSStatus err;
	err = AudioObjectSetPropertyData(dev_id,
					 &aopa,
					 0,
					 NULL,
					 size,
					 &hog_pid);
	if (err != noErr) {
		FmtDebug(osx_output_domain,
			 "Cannot hog the device: {}", err);
	} else {
		LogDebug(osx_output_domain,
			 hog_pid == -1
			 ? "Device is unhogged"
			 : "Device is hogged");
	}
}

[[gnu::pure]]
static bool
IsAudioDeviceName(AudioDeviceID id, const char *expected_name) noexcept
{
	static constexpr AudioObjectPropertyAddress aopa_name{
		kAudioObjectPropertyName,
		kAudioObjectPropertyScopeGlobal,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
	};

	char actual_name[256];

	try {
		auto cfname = AudioObjectGetStringProperty(id, aopa_name);
		if (!cfname.GetCString(actual_name, sizeof(actual_name)))
			return false;
	} catch (...) {
		return false;
	}

	return StringIsEqual(actual_name, expected_name);
}

static AudioDeviceID
FindAudioDeviceByName(const char *name)
{
	/* what are the available audio device IDs? */
	static constexpr AudioObjectPropertyAddress aopa_hw_devices{
		kAudioHardwarePropertyDevices,
		kAudioObjectPropertyScopeGlobal,
		KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
	};

	const auto ids =
		AudioObjectGetPropertyDataArray<AudioDeviceID>(kAudioObjectSystemObject,
							       aopa_hw_devices);

	for (const auto id : ids) {
		if (IsAudioDeviceName(id, name))
			return id;
	}

	throw FmtRuntimeError("Found no audio device names {:?}", name);
}

static void
osx_output_set_device(OSXOutput *oo)
{
	if (oo->component_subtype != kAudioUnitSubType_HALOutput)
		return;

	const auto id = FindAudioDeviceByName(oo->device_name);

	FmtDebug(osx_output_domain,
		 "found matching device: ID={}, name={}",
		 id, oo->device_name);

	AudioUnitSetCurrentDevice(oo->au, id);

	oo->dev_id = id;
	FmtDebug(osx_output_domain,
		 "set OS X audio output device ID={}, name={}",
		 id, oo->device_name);

	if (oo->channel_map)
		osx_output_set_channel_map(oo);
}


/**
 * This function (the 'render callback' osx_render) is called by the
 * OS X audio subsystem (CoreAudio) to request audio data that will be
 * played by the audio hardware. This function has hard time
 * constraints so it cannot do IO (debug statements) or memory
 * allocations.
 */
static OSStatus
osx_render(void *vdata,
	   [[maybe_unused]] AudioUnitRenderActionFlags *io_action_flags,
	   [[maybe_unused]] const AudioTimeStamp *in_timestamp,
	   [[maybe_unused]] UInt32 in_bus_number,
	   UInt32 in_number_frames,
	   AudioBufferList *buffer_list)
{
	OSXOutput *od = (OSXOutput *) vdata;

	std::size_t count = in_number_frames * od->asbd.mBytesPerFrame;
	buffer_list->mBuffers[0].mDataByteSize =
		od->ring_buffer.ReadTo({(std::byte *)buffer_list->mBuffers[0].mData, count});
	return noErr;
}

void
OSXOutput::Enable()
{
	AudioComponentDescription desc;
	desc.componentType = kAudioUnitType_Output;
	desc.componentSubType = component_subtype;
	desc.componentManufacturer = kAudioUnitManufacturer_Apple;
	desc.componentFlags = 0;
	desc.componentFlagsMask = 0;

	AudioComponent comp = AudioComponentFindNext(nullptr, &desc);
	if (comp == 0)
		throw std::runtime_error("Error finding OS X component");

	OSStatus status = AudioComponentInstanceNew(comp, &au);
	if (status != noErr)
		Apple::ThrowOSStatus(status, "Unable to open OS X component");

#ifdef ENABLE_DSD
	pcm_export.Construct();
#endif

	try {
		osx_output_set_device(this);
	} catch (...) {
		AudioComponentInstanceDispose(au);
#ifdef ENABLE_DSD
		pcm_export.Destruct();
#endif
		throw;
	}

	if (hog_device)
		osx_output_hog_device(dev_id, true);
}

void
OSXOutput::Disable() noexcept
{
	AudioComponentInstanceDispose(au);
#ifdef ENABLE_DSD
	pcm_export.Destruct();
#endif

	if (hog_device)
		osx_output_hog_device(dev_id, false);
}

void
OSXOutput::Close() noexcept
{
	if (started)
		AudioOutputUnitStop(au);
	AudioUnitUninitialize(au);
	ring_buffer = {};
}

void
OSXOutput::Open(AudioFormat &audio_format)
{
#ifdef ENABLE_DSD
	PcmExport::Params params;
	params.alsa_channel_order = true;
	bool dop = dop_setting;
#endif

	memset(&asbd, 0, sizeof(asbd));
	asbd.mFormatID = kAudioFormatLinearPCM;
	if (audio_format.format == SampleFormat::FLOAT) {
		asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat;
	} else {
		asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
	}

	if (IsBigEndian())
		asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;

	if (audio_format.format == SampleFormat::S24_P32) {
		asbd.mBitsPerChannel = 24;
	} else {
		asbd.mBitsPerChannel = audio_format.GetSampleSize() * 8;
	}
	asbd.mBytesPerPacket = audio_format.GetFrameSize();
	asbd.mSampleRate = audio_format.sample_rate;

#ifdef ENABLE_DSD
	if (dop && audio_format.format == SampleFormat::DSD) {
		asbd.mBitsPerChannel = 24;
		params.dsd_mode = PcmExport::DsdMode::DOP;
		asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate);
		asbd.mBytesPerPacket = 4 * audio_format.channels;

	}
#endif

	asbd.mFramesPerPacket = 1;
	asbd.mBytesPerFrame = asbd.mBytesPerPacket;
	asbd.mChannelsPerFrame = audio_format.channels;

	Float64 sample_rate = osx_output_set_device_format(dev_id, asbd);

#ifdef ENABLE_DSD
	if (audio_format.format == SampleFormat::DSD &&
	    sample_rate != asbd.mSampleRate) {
		// fall back to PCM in case sample_rate cannot be synchronized
		params.dsd_mode = PcmExport::DsdMode::NONE;
		audio_format.format = SampleFormat::S32;
		asbd.mBitsPerChannel = 32;
		asbd.mBytesPerPacket = audio_format.GetFrameSize();
		asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate);
		asbd.mBytesPerFrame = asbd.mBytesPerPacket;
	}
	dop_enabled = params.dsd_mode == PcmExport::DsdMode::DOP;
#endif

	AudioUnitSetInputStreamFormat(au, asbd);

	AURenderCallbackStruct callback;
	callback.inputProc = osx_render;
	callback.inputProcRefCon = this;

	AudioUnitSetInputRenderCallback(au, callback);

	OSStatus status = AudioUnitInitialize(au);
	if (status != noErr)
		Apple::ThrowOSStatus(status, "Unable to initialize OS X audio unit");

	UInt32 buffer_frame_size = osx_output_set_buffer_size(au, asbd);

	size_t ring_buffer_size = std::max<size_t>(buffer_frame_size,
						   MPD_OSX_BUFFER_TIME_MS * audio_format.GetFrameSize() * audio_format.sample_rate / 1000);

#ifdef ENABLE_DSD
	if (dop_enabled) {
		pcm_export->Open(audio_format.format, audio_format.channels, params);
		ring_buffer_size = std::max<size_t>(buffer_frame_size,
						   MPD_OSX_BUFFER_TIME_MS * pcm_export->GetOutputFrameSize() * asbd.mSampleRate / 1000);
	}
#endif
	ring_buffer = RingBuffer{ring_buffer_size};

	pause = false;
	started = false;
}

std::size_t
OSXOutput::Play(std::span<const std::byte> input)
{
	assert(!input.empty());

	pause = false;

#ifdef ENABLE_DSD
	if (dop_enabled) {
		input = pcm_export->Export(input);
		if (input.empty())
			return input.size();
	}
#endif

	size_t bytes_written = ring_buffer.WriteFrom(input);

	if (!started) {
		OSStatus status = AudioOutputUnitStart(au);
		if (status != noErr)
			throw std::runtime_error("Unable to restart audio output after pause");

		started = true;
	}

#ifdef ENABLE_DSD
	if (dop_enabled)
		bytes_written = pcm_export->CalcInputSize(bytes_written);
#endif

	return bytes_written;
}

std::chrono::steady_clock::duration
OSXOutput::Delay() const noexcept
{
	return !ring_buffer.IsFull() && !pause
		? std::chrono::steady_clock::duration::zero()
		: std::chrono::milliseconds(MPD_OSX_BUFFER_TIME_MS / 4);
}

bool OSXOutput::Pause()
{
	pause = true;

	if (started) {
		AudioOutputUnitStop(au);
		started = false;
	}

	return true;
}

void
OSXOutput::Cancel() noexcept
{
	if (started) {
		AudioOutputUnitStop(au);
		started = false;
	}

	ring_buffer.Clear();
#ifdef ENABLE_DSD
	pcm_export->Reset();
#endif

	/* the AudioUnit will be restarted by the next Play() call */
}

int
osx_output_get_volume(OSXOutput &output)
{
	return output.GetVolume();
}

void
osx_output_set_volume(OSXOutput &output, unsigned new_volume)
{
	return output.SetVolume(new_volume);
}

const struct AudioOutputPlugin osx_output_plugin = {
	"osx",
	osx_output_test_default_device,
	&OSXOutput::Create,
	&osx_mixer_plugin,
};
