/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Audio Input Redirection Virtual Channel - Mac OS X implementation
 *
 * Copyright (c) 2015 Armin Novak <armin.novak@thincast.com>
 * Copyright 2015 Thincast Technologies GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <freerdp/config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <winpr/crt.h>
#include <winpr/synch.h>
#include <winpr/string.h>
#include <winpr/thread.h>
#include <winpr/debug.h>
#include <winpr/cmdline.h>

#import <AVFoundation/AVFoundation.h>

#define __COREFOUNDATION_CFPLUGINCOM__ 1
#define IUNKNOWN_C_GUTS   \
	void *_reserved;      \
	void *QueryInterface; \
	void *AddRef;         \
	void *Release

#include <CoreAudio/CoreAudioTypes.h>
#include <CoreAudio/CoreAudio.h>
#include <AudioToolbox/AudioToolbox.h>
#include <AudioToolbox/AudioQueue.h>

#include <freerdp/addin.h>
#include <freerdp/channels/rdpsnd.h>

#include "audin_main.h"

#define MAC_AUDIO_QUEUE_NUM_BUFFERS 100

/* Fix for #4462: Provide type alias if not declared (Mac OS < 10.10)
 * https://developer.apple.com/documentation/coreaudio/audioformatid
 */
#ifndef AudioFormatID
typedef UInt32 AudioFormatID;
#endif

#ifndef AudioFormatFlags
typedef UInt32 AudioFormatFlags;
#endif

typedef struct
{
	IAudinDevice iface;

	AUDIO_FORMAT format;
	UINT32 FramesPerPacket;
	int dev_unit;

	AudinReceive receive;
	void *user_data;

	rdpContext *rdpcontext;

	bool isAuthorized;
	bool isOpen;
	AudioQueueRef audioQueue;
	AudioStreamBasicDescription audioFormat;
	AudioQueueBufferRef audioBuffers[MAC_AUDIO_QUEUE_NUM_BUFFERS];
} AudinMacDevice;

static AudioFormatID audin_mac_get_format(const AUDIO_FORMAT *format)
{
	switch (format->wFormatTag)
	{
		case WAVE_FORMAT_PCM:
			return kAudioFormatLinearPCM;

		default:
			return 0;
	}
}

static AudioFormatFlags audin_mac_get_flags_for_format(const AUDIO_FORMAT *format)
{
	switch (format->wFormatTag)
	{
		case WAVE_FORMAT_PCM:
			return kAudioFormatFlagIsSignedInteger;

		default:
			return 0;
	}
}

static BOOL audin_mac_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format)
{
	AudinMacDevice *mac = (AudinMacDevice *)device;
	AudioFormatID req_fmt = 0;

	if (!mac->isAuthorized)
		return FALSE;

	if (device == nullptr || format == nullptr)
		return FALSE;

	if (format->nChannels != 2)
		return FALSE;

	req_fmt = audin_mac_get_format(format);

	if (req_fmt == 0)
		return FALSE;

	return TRUE;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT audin_mac_set_format(IAudinDevice *device, const AUDIO_FORMAT *format,
                                 UINT32 FramesPerPacket)
{
	AudinMacDevice *mac = (AudinMacDevice *)device;

	if (!mac->isAuthorized)
		return ERROR_INTERNAL_ERROR;

	if (device == nullptr || format == nullptr)
		return ERROR_INVALID_PARAMETER;

	mac->FramesPerPacket = FramesPerPacket;
	mac->format = *format;
	WLog_INFO(TAG, "Audio Format %s [channels=%d, samples=%d, bits=%d]",
	          audio_format_get_tag_string(format->wFormatTag), format->nChannels,
	          format->nSamplesPerSec, format->wBitsPerSample);
	mac->audioFormat.mBitsPerChannel = format->wBitsPerSample;

	if (format->wBitsPerSample == 0)
		mac->audioFormat.mBitsPerChannel = 16;

	mac->audioFormat.mChannelsPerFrame = mac->format.nChannels;
	mac->audioFormat.mFramesPerPacket = 1;

	mac->audioFormat.mBytesPerFrame =
	    mac->audioFormat.mChannelsPerFrame * (mac->audioFormat.mBitsPerChannel / 8);
	mac->audioFormat.mBytesPerPacket =
	    mac->audioFormat.mBytesPerFrame * mac->audioFormat.mFramesPerPacket;

	mac->audioFormat.mFormatFlags = audin_mac_get_flags_for_format(format);
	mac->audioFormat.mFormatID = audin_mac_get_format(format);
	mac->audioFormat.mReserved = 0;
	mac->audioFormat.mSampleRate = mac->format.nSamplesPerSec;
	return CHANNEL_RC_OK;
}

static void mac_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer,
                                     const AudioTimeStamp *inStartTime, UInt32 inNumPackets,
                                     const AudioStreamPacketDescription *inPacketDesc)
{
	AudinMacDevice *mac = (AudinMacDevice *)aqData;
	UINT error = CHANNEL_RC_OK;
	const BYTE *buffer = inBuffer->mAudioData;
	int buffer_size = inBuffer->mAudioDataByteSize;
	(void)inAQ;
	(void)inStartTime;
	(void)inNumPackets;
	(void)inPacketDesc;

	if (buffer_size > 0)
		error = mac->receive(&mac->format, buffer, buffer_size, mac->user_data);

	AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);

	if (error)
	{
		WLog_ERR(TAG, "mac->receive failed with error %" PRIu32 "", error);
		SetLastError(ERROR_INTERNAL_ERROR);
	}
}

static UINT audin_mac_close(IAudinDevice *device)
{
	UINT errCode = CHANNEL_RC_OK;
	char errString[1024];
	OSStatus devStat;
	AudinMacDevice *mac = (AudinMacDevice *)device;

	if (!mac->isAuthorized)
		return ERROR_INTERNAL_ERROR;

	if (device == nullptr)
		return ERROR_INVALID_PARAMETER;

	if (mac->isOpen)
	{
		devStat = AudioQueueStop(mac->audioQueue, true);

		if (devStat != 0)
		{
			errCode = GetLastError();
			WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]",
			         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
		}

		mac->isOpen = false;
	}

	if (mac->audioQueue)
	{
		devStat = AudioQueueDispose(mac->audioQueue, true);

		if (devStat != 0)
		{
			errCode = GetLastError();
			WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]",
			         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
		}

		mac->audioQueue = nullptr;
	}

	mac->receive = nullptr;
	mac->user_data = nullptr;
	return errCode;
}

static UINT audin_mac_open(IAudinDevice *device, AudinReceive receive, void *user_data)
{
	AudinMacDevice *mac = (AudinMacDevice *)device;
	DWORD errCode;
	char errString[1024];
	OSStatus devStat;

	if (!mac->isAuthorized)
		return ERROR_INTERNAL_ERROR;

	mac->receive = receive;
	mac->user_data = user_data;
	devStat = AudioQueueNewInput(&(mac->audioFormat), mac_audio_queue_input_cb, mac, nullptr,
	                             kCFRunLoopCommonModes, 0, &(mac->audioQueue));

	if (devStat != 0)
	{
		errCode = GetLastError();
		WLog_ERR(TAG, "AudioQueueNewInput failed with %s [%" PRIu32 "]",
		         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
		goto err_out;
	}

	for (size_t index = 0; index < MAC_AUDIO_QUEUE_NUM_BUFFERS; index++)
	{
		devStat = AudioQueueAllocateBuffer(mac->audioQueue,
		                                   mac->FramesPerPacket * 2 * mac->format.nChannels,
		                                   &mac->audioBuffers[index]);

		if (devStat != 0)
		{
			errCode = GetLastError();
			WLog_ERR(TAG, "AudioQueueAllocateBuffer failed with %s [%" PRIu32 "]",
			         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
			goto err_out;
		}

		devStat = AudioQueueEnqueueBuffer(mac->audioQueue, mac->audioBuffers[index], 0, nullptr);

		if (devStat != 0)
		{
			errCode = GetLastError();
			WLog_ERR(TAG, "AudioQueueEnqueueBuffer failed with %s [%" PRIu32 "]",
			         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
			goto err_out;
		}
	}

	devStat = AudioQueueStart(mac->audioQueue, nullptr);

	if (devStat != 0)
	{
		errCode = GetLastError();
		WLog_ERR(TAG, "AudioQueueStart failed with %s [%" PRIu32 "]",
		         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
		goto err_out;
	}

	mac->isOpen = true;
	return CHANNEL_RC_OK;
err_out:
	audin_mac_close(device);
	return CHANNEL_RC_INITIALIZATION_ERROR;
}

static UINT audin_mac_free(IAudinDevice *device)
{
	AudinMacDevice *mac = (AudinMacDevice *)device;
	int error;

	if (device == nullptr)
		return ERROR_INVALID_PARAMETER;

	if ((error = audin_mac_close(device)))
	{
		WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error);
	}

	free(mac);
	return CHANNEL_RC_OK;
}

static UINT audin_mac_parse_addin_args(AudinMacDevice *device, const ADDIN_ARGV *args)
{
	DWORD errCode;
	char errString[1024];
	int status;
	char *str_num, *eptr;
	DWORD flags;
	const COMMAND_LINE_ARGUMENT_A *arg;
	COMMAND_LINE_ARGUMENT_A audin_mac_args[] = {
		{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
		  "audio device name" },
		{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
	};

	AudinMacDevice *mac = (AudinMacDevice *)device;

	if (args->argc == 1)
		return CHANNEL_RC_OK;

	flags =
	    COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
	status = CommandLineParseArgumentsA(args->argc, args->argv, audin_mac_args, flags, mac, nullptr,
	                                    nullptr);

	if (status < 0)
		return ERROR_INVALID_PARAMETER;

	arg = audin_mac_args;

	do
	{
		if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
			continue;

		CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
		{
			str_num = _strdup(arg->Value);

			if (!str_num)
			{
				errCode = GetLastError();
				WLog_ERR(TAG, "_strdup failed with %s [%d]",
				         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
				return CHANNEL_RC_NO_MEMORY;
			}

			mac->dev_unit = strtol(str_num, &eptr, 10);

			if (mac->dev_unit < 0 || *eptr != '\0')
				mac->dev_unit = -1;

			free(str_num);
		}
		CommandLineSwitchEnd(arg)
	} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);

	return CHANNEL_RC_OK;
}

FREERDP_ENTRY_POINT(UINT VCAPITYPE mac_freerdp_audin_client_subsystem_entry(
    PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
{
	DWORD errCode;
	char errString[1024];
	const ADDIN_ARGV *args;
	AudinMacDevice *mac;
	UINT error;
	mac = (AudinMacDevice *)calloc(1, sizeof(AudinMacDevice));

	if (!mac)
	{
		errCode = GetLastError();
		WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]",
		         winpr_strerror(errCode, errString, sizeof(errString)), errCode);
		return CHANNEL_RC_NO_MEMORY;
	}

	mac->iface.Open = audin_mac_open;
	mac->iface.FormatSupported = audin_mac_format_supported;
	mac->iface.SetFormat = audin_mac_set_format;
	mac->iface.Close = audin_mac_close;
	mac->iface.Free = audin_mac_free;
	mac->rdpcontext = pEntryPoints->rdpcontext;
	mac->dev_unit = -1;
	args = pEntryPoints->args;

	if ((error = audin_mac_parse_addin_args(mac, args)))
	{
		WLog_ERR(TAG, "audin_mac_parse_addin_args failed with %" PRIu32 "!", error);
		goto error_out;
	}

	if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)mac)))
	{
		WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error);
		goto error_out;
	}

#if defined(MAC_OS_X_VERSION_10_14)
	if (@available(macOS 10.14, *))
	{
		@autoreleasepool
		{
			AVAuthorizationStatus status =
			    [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
			switch (status)
			{
				case AVAuthorizationStatusAuthorized:
					mac->isAuthorized = TRUE;
					break;
				case AVAuthorizationStatusNotDetermined:
					[AVCaptureDevice
					    requestAccessForMediaType:AVMediaTypeAudio
					            completionHandler:^(BOOL granted) {
						            if (granted == YES)
						            {
							            mac->isAuthorized = TRUE;
						            }
						            else
							            WLog_WARN(TAG, "Microphone access denied by user");
					            }];
					break;
				case AVAuthorizationStatusRestricted:
					WLog_WARN(TAG, "Microphone access restricted by policy");
					break;
				case AVAuthorizationStatusDenied:
					WLog_WARN(TAG, "Microphone access denied by policy");
					break;
				default:
					break;
			}
		}
	}
#endif

	return CHANNEL_RC_OK;
error_out:
	free(mac);
	return error;
}
