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

#include "../../obs-internal.h"
#include "../../util/dstr.h"

#include "mac-helpers.h"

static inline bool cf_to_cstr(CFStringRef ref, char *buf, size_t size)
{
	if (!ref) return false;
	return (bool)CFStringGetCString(ref, buf, size, kCFStringEncodingUTF8);
}

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[1024];
	char        uid[1024];
	OSStatus    stat;
	bool        cont = true;

	AudioObjectPropertyAddress addr = {
		kAudioDevicePropertyStreams,
		kAudioDevicePropertyScopeInput,
		kAudioObjectPropertyElementMaster
	};

	/* check to see if it's a mac input device */
	if (!allow_inputs) {
		AudioObjectGetPropertyDataSize(id, &addr, 0, NULL, &size);
		if (!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"))
		return true;

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

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

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

	cont = cb(data, name, uid);

fail:
	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,
		kAudioObjectPropertyElementMaster
	};

	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 = {
		kAudioHardwarePropertyDefaultSystemOutputDevice,
		kAudioObjectPropertyScopeGlobal,
		kAudioObjectPropertyElementMaster
	};

	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;
}
