#include <CoreFoundation/CFString.h>
#include <CoreAudio/CoreAudio.h>

#include "../../obs-internal.h"
#include "../../util/dstr.h"
#include "../../util/apple/cfstring-utils.h"

#include "mac-helpers.h"

static bool obs_enum_audio_monitoring_device(obs_enum_audio_device_cb cb, void *data, AudioDeviceID id,
					     bool allow_inputs)
{
	UInt32 size = 0;
	CFStringRef cf_name = NULL;
	CFStringRef cf_uid = NULL;
	char *name = NULL;
	char *uid = NULL;
	OSStatus stat;
	bool cont = true;

	AudioObjectPropertyAddress addr = {kAudioDevicePropertyStreams, kAudioDevicePropertyScopeOutput,
					   kAudioObjectPropertyElementMain};

	/* Check if the device is capable of audio output. */
	AudioObjectGetPropertyDataSize(id, &addr, 0, NULL, &size);
	if (!allow_inputs && !size)
		return true;

	size = sizeof(CFStringRef);

	addr.mSelector = kAudioDevicePropertyDeviceUID;
	stat = AudioObjectGetPropertyData(id, &addr, 0, NULL, &size, &cf_uid);
	if (!success(stat, "get audio device UID"))
		goto fail;

	addr.mSelector = kAudioDevicePropertyDeviceNameCFString;
	stat = AudioObjectGetPropertyData(id, &addr, 0, NULL, &size, &cf_name);
	if (!success(stat, "get audio device name"))
		goto fail;

	name = cfstr_copy_cstr(cf_name, kCFStringEncodingUTF8);
	if (!name) {
		blog(LOG_WARNING, "%s: failed to convert name", __FUNCTION__);
		goto fail;
	}

	uid = cfstr_copy_cstr(cf_uid, kCFStringEncodingUTF8);
	if (!uid) {
		blog(LOG_WARNING, "%s: failed to convert uid", __FUNCTION__);
		goto fail;
	}

	cont = cb(data, name, uid);

fail:
	bfree(name);
	bfree(uid);
	if (cf_name)
		CFRelease(cf_name);
	if (cf_uid)
		CFRelease(cf_uid);
	return cont;
}

static void enum_audio_devices(obs_enum_audio_device_cb cb, void *data, bool allow_inputs)
{
	AudioObjectPropertyAddress addr = {kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal,
					   kAudioObjectPropertyElementMain};

	UInt32 size = 0;
	UInt32 count;
	OSStatus stat;
	AudioDeviceID *ids;

	stat = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, 0, NULL, &size);
	if (!success(stat, "get data size"))
		return;

	ids = malloc(size);
	count = size / sizeof(AudioDeviceID);

	stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, ids);
	if (success(stat, "get data")) {
		for (UInt32 i = 0; i < count; i++) {
			if (!obs_enum_audio_monitoring_device(cb, data, ids[i], allow_inputs))
				break;
		}
	}

	free(ids);
}

void obs_enum_audio_monitoring_devices(obs_enum_audio_device_cb cb, void *data)
{
	enum_audio_devices(cb, data, false);
}

static bool alloc_default_id(void *data, const char *name, const char *id)
{
	char **p_id = data;
	UNUSED_PARAMETER(name);

	*p_id = bstrdup(id);
	return false;
}

static void get_default_id(char **p_id)
{
	AudioObjectPropertyAddress addr = {kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal,
					   kAudioObjectPropertyElementMain};

	if (*p_id)
		return;

	OSStatus stat;
	AudioDeviceID id = 0;
	UInt32 size = sizeof(id);

	stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, &id);
	if (success(stat, "AudioObjectGetPropertyData"))
		obs_enum_audio_monitoring_device(alloc_default_id, p_id, id, true);
	if (!*p_id)
		*p_id = bzalloc(1);
}

struct device_name_info {
	const char *id;
	char *name;
};

static bool enum_device_name(void *data, const char *name, const char *id)
{
	struct device_name_info *info = data;

	if (strcmp(info->id, id) == 0) {
		info->name = bstrdup(name);
		return false;
	}

	return true;
}

bool devices_match(const char *id1, const char *id2)
{
	struct device_name_info info = {0};
	char *default_id = NULL;
	char *name1 = NULL;
	char *name2 = NULL;
	bool match;

	if (!id1 || !id2)
		return false;

	if (strcmp(id1, "default") == 0) {
		get_default_id(&default_id);
		id1 = default_id;
	}
	if (strcmp(id2, "default") == 0) {
		get_default_id(&default_id);
		id2 = default_id;
	}

	info.id = id1;
	enum_audio_devices(enum_device_name, &info, true);
	name1 = info.name;

	info.name = NULL;
	info.id = id2;
	enum_audio_devices(enum_device_name, &info, true);
	name2 = info.name;

	match = name1 && name2 && strcmp(name1, name2) == 0;
	bfree(default_id);
	bfree(name1);
	bfree(name2);

	return match;
}

static inline bool device_is_input(const char *device)
{
	return astrstri(device, "soundflower") == NULL && astrstri(device, "wavtap") == NULL &&
	       astrstri(device, "soundsiphon") == NULL && astrstri(device, "ishowu") == NULL &&
	       astrstri(device, "blackhole") == NULL && astrstri(device, "loopback") == NULL &&
	       astrstri(device, "groundcontrol") == NULL && astrstri(device, "vbcable") == NULL;
}

static bool find_loopback_cb(void *param, const char *name, const char *id)
{
	UNUSED_PARAMETER(name);
	char **p_id = param;

	if (!device_is_input(id)) {
		*p_id = bstrdup(id);
		return false;
	}
	return true;
}

void get_desktop_default_id(char **p_id)
{
	if (*p_id)
		return;

	AudioObjectPropertyAddress addr = {kAudioHardwarePropertyDefaultSystemOutputDevice,
					   kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};

	AudioDeviceID id = 0;
	UInt32 size = sizeof(id);
	OSStatus stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, &id);

	if (success(stat, "AudioObjectGetPropertyData")) {
		/* Try system default output first */
		obs_enum_audio_monitoring_device(alloc_default_id, p_id, id, false);

		/* If not a loopback, try to find a virtual (non-input) device instead */
		if (*p_id && device_is_input(*p_id)) {
			bfree(*p_id);
			*p_id = NULL;
			enum_audio_devices(find_loopback_cb, p_id, false);
		}
	}

	if (!*p_id)
		*p_id = bzalloc(1);
}
