//-----------------------------------------------------------------------------
// Project     : VST SDK
//
// Category    : Helpers
// Filename    : public.sdk/source/vst/interappaudio/AudioIO.mm
// Created by  : Steinberg, 08/2013.
// Description : VST 3 InterAppAudio
// Flags       : clang-format SMTGSequencer
//
//-----------------------------------------------------------------------------
// This file is part of a Steinberg SDK. It is subject to the license terms
// in the LICENSE file found in the top-level directory of this distribution
// and at www.steinberg.net/sdklicenses.
// No part of the SDK, including this file, may be copied, modified, propagated,
// or distributed except according to the terms contained in the LICENSE file.
//-----------------------------------------------------------------------------

#import "AudioIO.h"

#import "MidiIO.h"
#import "pluginterfaces/base/fstrdefs.h"
#import <AVFoundation/AVAudioSession.h>
#import <AudioUnit/AudioUnit.h>
#import <UIKit/UIKit.h>

#define FORCE_INLINE __attribute__ ((always_inline))

namespace Steinberg {
namespace Vst {
namespace InterAppAudio {

//------------------------------------------------------------------------
static AudioBufferList* createBuffers (uint32 numChannels, uint32 maxFrames, uint32 frameSize)
{
	AudioBufferList* result =
	    (AudioBufferList*)malloc (sizeof (AudioBufferList) + sizeof (AudioBuffer) * numChannels);
	result->mNumberBuffers = numChannels;
	for (int32 i = 0; i < numChannels; i++)
	{
		result->mBuffers[i].mDataByteSize = maxFrames * sizeof (float);
		result->mBuffers[i].mData = calloc (1, result->mBuffers[i].mDataByteSize);
		result->mBuffers[i].mNumberChannels = 1;
	}
	return result;
}

//------------------------------------------------------------------------
static void freeAudioBufferList (AudioBufferList* audioBufferList)
{
	for (uint32 i = 0; i < audioBufferList->mNumberBuffers; i++)
	{
		free (audioBufferList->mBuffers[i].mData);
	}
	free (audioBufferList);
}

//------------------------------------------------------------------------
NSString* AudioIO::kConnectionStateChange = @"AudioIO::kConnectionStateChange";

//------------------------------------------------------------------------
AudioIO::AudioIO ()
{
	sampleRate = [[AVAudioSession sharedInstance] sampleRate];
	MidiIO::instance ();
}

//------------------------------------------------------------------------
AudioIO::~AudioIO ()
{
	if (ioBufferList)
		freeAudioBufferList (ioBufferList);
}

//------------------------------------------------------------------------
AudioIO* AudioIO::instance ()
{
	static AudioIO gInstance;
	return &gInstance;
}

//------------------------------------------------------------------------
tresult AudioIO::setupRemoteIO (OSType type)
{
	if (remoteIO != nullptr)
	{
		AudioStreamBasicDescription streamFormat = {};
		streamFormat.mChannelsPerFrame = 2;
		streamFormat.mSampleRate = sampleRate;
		streamFormat.mFormatID = kAudioFormatLinearPCM;
		streamFormat.mFormatFlags =
		    kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
		streamFormat.mBytesPerFrame = streamFormat.mBytesPerPacket = sizeof (Float32);
		streamFormat.mBitsPerChannel = 32;
		streamFormat.mFramesPerPacket = 1;

		OSStatus status =
		    AudioUnitSetProperty (remoteIO, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
		                          1, &streamFormat, sizeof (streamFormat));
		if (status != noErr)
			return kInternalError;

		status =
		    AudioUnitSetProperty (remoteIO, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
		                          0, &streamFormat, sizeof (streamFormat));
		if (status != noErr)
			return kInternalError;

		status = AudioUnitSetProperty (remoteIO, kAudioUnitProperty_MaximumFramesPerSlice,
		                               kAudioUnitScope_Global, 1, &maxFrames, sizeof (maxFrames));

		if (status != noErr)
			return kInternalError;

		bool needInput = (type == kAudioUnitType_RemoteGenerator ||
		                  type == kAudioUnitType_RemoteInstrument) == false;
		UInt32 flag = 1;
		if (needInput)
		{
			// enable IO Input
			status = AudioUnitSetProperty (remoteIO, kAudioOutputUnitProperty_EnableIO,
			                               kAudioUnitScope_Input, 1, &flag, sizeof (flag));
			if (status != noErr)
				return kInternalError;
		}

		// enable IO Output
		status = AudioUnitSetProperty (remoteIO, kAudioOutputUnitProperty_EnableIO,
		                               kAudioUnitScope_Output, 0, &flag, sizeof (flag));
		if (status != noErr)
			return kInternalError;

		AURenderCallbackStruct renderCallback = {};
		if (needInput)
		{
			renderCallback.inputProc = inputCallbackStatic;
			renderCallback.inputProcRefCon = this;
			status = AudioUnitSetProperty (remoteIO, kAudioOutputUnitProperty_SetInputCallback,
			                               kAudioUnitScope_Global, 1, &renderCallback,
			                               sizeof (renderCallback));
			if (status != noErr)
				return kInternalError;
		}

		renderCallback.inputProc = renderCallbackStatic;
		renderCallback.inputProcRefCon = this;
		status = AudioUnitSetProperty (remoteIO, kAudioUnitProperty_SetRenderCallback,
		                               kAudioUnitScope_Global, 0, &renderCallback,
		                               sizeof (renderCallback));
		if (status != noErr)
			return kInternalError;

		if (type == kAudioUnitType_RemoteInstrument || type == kAudioUnitType_RemoteMusicEffect)
		{
			AudioOutputUnitMIDICallbacks callBackStruct = {};
			callBackStruct.userData = this;
			callBackStruct.MIDIEventProc = midiEventCallbackStatic;
			status = AudioUnitSetProperty (remoteIO, kAudioOutputUnitProperty_MIDICallbacks,
			                               kAudioUnitScope_Global, 0, &callBackStruct,
			                               sizeof (callBackStruct));
			if (status != noErr)
			{
				NSLog (@"Setting MIDICallback on OutputUnit failed");
			}
		}

		if (ioBufferList)
			freeAudioBufferList (ioBufferList);
		ioBufferList =
		    createBuffers (streamFormat.mChannelsPerFrame, maxFrames, streamFormat.mBytesPerFrame);
		if (ioBufferList == nullptr)
			return kOutOfMemory;

		status = AudioUnitAddPropertyListener (remoteIO, kAudioUnitProperty_IsInterAppConnected,
		                                       propertyChangeStatic, this);
		status = AudioUnitAddPropertyListener (
		    remoteIO, kAudioOutputUnitProperty_HostTransportState, propertyChangeStatic, this);

		return kResultTrue;
	}
	return kResultFalse;
}

//------------------------------------------------------------------------
tresult AudioIO::setupAUGraph (OSType type)
{
	if (graph == nullptr)
	{
		OSStatus status = NewAUGraph (&graph);
		if (status != noErr)
			return kInternalError;

		AudioComponentDescription iOUnitDescription;
		iOUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
		iOUnitDescription.componentFlags = 0;
		iOUnitDescription.componentFlagsMask = 0;
		iOUnitDescription.componentType = kAudioUnitType_Output;
		iOUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;

		AUNode remoteIONode;
		status = AUGraphAddNode (graph, &iOUnitDescription, &remoteIONode);
		if (status != noErr)
			return kInternalError;
		status = AUGraphOpen (graph);
		if (status != noErr)
			return kInternalError;
		status = AUGraphNodeInfo (graph, remoteIONode, nullptr, &remoteIO);
		if (status != noErr)
			return kInternalError;
		return setupRemoteIO (type);
	}
	return kResultFalse;
}

//------------------------------------------------------------------------
void AudioIO::updateInterAppAudioConnectionState ()
{
	if (remoteIO)
	{
		UInt32 connected;
		UInt32 dataSize = sizeof (connected);
		OSStatus status = AudioUnitGetProperty (remoteIO, kAudioUnitProperty_IsInterAppConnected,
		                                        kAudioUnitScope_Global, 0, &connected, &dataSize);
		if (status == noErr)
		{
			if (interAppAudioConnected != connected)
			{
				if (connected)
				{
					UInt32 size = sizeof (HostCallbackInfo);
					status = AudioUnitGetProperty (remoteIO, kAudioUnitProperty_HostCallbacks,
					                               kAudioUnitScope_Global, 0, &hostCallback, &size);
				}
				else
				{
					memset (&hostCallback, 0, sizeof (HostCallbackInfo));
				}
				interAppAudioConnected = connected > 0 ? true : false;
				[[NSNotificationCenter defaultCenter] postNotificationName:kConnectionStateChange
				                                                    object:nil];
			}
		}
	}
}

//------------------------------------------------------------------------
tresult AudioIO::init (OSType type, OSType subType, OSType manufacturer, CFStringRef name)
{
	tresult result = setupAUGraph (type);
	if (result != kResultTrue)
		return result;

	AudioComponentDescription desc = {type, subType, manufacturer, 0, 0};
	OSStatus status = AudioOutputUnitPublish (&desc, name, 0, remoteIO);
	if (status != noErr)
	{
		NSLog (@"AudioOutputUnitPublish failed with status:%d", (int)status);
	}
	internalState = kInitialized;

	return result;
}

//------------------------------------------------------------------------
void AudioIO::setAudioSessionActive (bool state)
{
	NSError* error;

	AVAudioSession* session = [AVAudioSession sharedInstance];
	[session setPreferredSampleRate:sampleRate error:&error];
	[session setCategory:AVAudioSessionCategoryPlayback
	         withOptions:AVAudioSessionCategoryOptionMixWithOthers
	               error:&error];
	[session setActive:(state ? YES : NO)error:&error];
}

//------------------------------------------------------------------------
tresult AudioIO::start ()
{
	if (internalState == kInitialized)
	{
		bool appIsActive =
		    [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
		if (!(appIsActive || interAppAudioConnected))
		{
			return kResultFalse;
		}
		setAudioSessionActive (true);

		Boolean graphInitialized = true;
		OSStatus status = AUGraphIsInitialized (graph, &graphInitialized);
		if (status != noErr)
			return kInternalError;

		if (graphInitialized == false)
		{
			status = AUGraphInitialize (graph);
			if (status != noErr)
				return kInternalError;
			updateInterAppAudioConnectionState ();
		}

		for (auto processor : audioProcessors)
		{
			processor->willStartAudio (this);
		}
		status = AUGraphStart (graph);
		if (status == noErr)
		{
			internalState = kStarted;
			updateInterAppAudioConnectionState ();
			return kResultTrue;
		}
		return kInternalError;
	}

	return kResultFalse;
}

//------------------------------------------------------------------------
tresult AudioIO::stop ()
{
	if (internalState == kStarted)
	{
		if (AUGraphStop (graph) == noErr)
		{
			for (auto processor : audioProcessors)
			{
				processor->didStopAudio (this);
			}
			internalState = kInitialized;
			if (interAppAudioConnected == false)
				setAudioSessionActive (false);
			return kResultTrue;
		}
	}
	return kResultFalse;
}

//------------------------------------------------------------------------
tresult AudioIO::addProcessor (IAudioIOProcessor* processor)
{
	if (internalState == kInitialized)
	{
		audioProcessors.push_back (processor);
		MidiIO::instance ().addProcessor (processor);
		return kResultTrue;
	}
	return kResultFalse;
}

//------------------------------------------------------------------------
tresult AudioIO::removeProcessor (IAudioIOProcessor* processor)
{
	if (internalState == kInitialized)
	{
		auto it = std::find (audioProcessors.begin (), audioProcessors.end (), processor);
		if (it != audioProcessors.end ())
		{
			audioProcessors.erase (it);
			MidiIO::instance ().removeProcessor (processor);
			return kResultTrue;
		}
	}
	return kResultFalse;
}

//------------------------------------------------------------------------
bool AudioIO::switchToHost ()
{
	if (remoteIO && interAppAudioConnected)
	{
		CFURLRef instrumentUrl;
		UInt32 dataSize = sizeof (instrumentUrl);
		OSStatus result =
		    AudioUnitGetProperty (remoteIO, kAudioUnitProperty_PeerURL, kAudioUnitScope_Global, 0,
		                          &instrumentUrl, &dataSize);
		if (result == noErr)
		{
			[[UIApplication sharedApplication] openURL:(__bridge NSURL*)instrumentUrl];
			return true;
		}
	}
	return false;
}

//------------------------------------------------------------------------
bool AudioIO::sendRemoteControlEvent (AudioUnitRemoteControlEvent event)
{
	if (remoteIO && interAppAudioConnected)
	{
		UInt32 controlEvent = event;
		UInt32 dataSize = sizeof (controlEvent);
		OSStatus status =
		    AudioUnitSetProperty (remoteIO, kAudioOutputUnitProperty_RemoteControlToHost,
		                          kAudioUnitScope_Global, 0, &controlEvent, dataSize);
		return status == noErr;
	}
	return false;
}

//------------------------------------------------------------------------
UIImage* AudioIO::getHostIcon ()
{
	if (remoteIO && interAppAudioConnected)
	{
		return AudioOutputUnitGetHostIcon (remoteIO, 128);
	}
	return nil;
}

//------------------------------------------------------------------------
bool AudioIO::getBeatAndTempo (Float64& beat, Float64& tempo)
{
	if (hostCallback.beatAndTempoProc)
	{
		if (hostCallback.beatAndTempoProc (hostCallback.hostUserData, &beat, &tempo) == noErr)
			return true;
	}
	tempo = staticTempo;
	beat = 0;
	return true;
}

//------------------------------------------------------------------------
bool AudioIO::getMusicalTimeLocation (UInt32& deltaSampleOffset, Float32& timeSigNumerator,
                                      UInt32& timeSigDenominator, Float64& downBeat)
{
	if (hostCallback.musicalTimeLocationProc)
	{
		if (hostCallback.musicalTimeLocationProc (hostCallback.hostUserData, &deltaSampleOffset,
		                                          &timeSigNumerator, &timeSigDenominator,
		                                          &downBeat) == noErr)
			return true;
	}
	return false;
}

//------------------------------------------------------------------------
bool AudioIO::getTransportState (Boolean& isPlaying, Boolean& isRecording,
                                 Boolean& transportStateChanged, Float64& sampleInTimeLine,
                                 Boolean& isCycling, Float64& cycleStartBeat, Float64& cycleEndBeat)
{
	if (hostCallback.transportStateProc2)
	{
		if (hostCallback.transportStateProc2 (hostCallback.hostUserData, &isPlaying, &isRecording,
		                                      &transportStateChanged, &sampleInTimeLine, &isCycling,
		                                      &cycleStartBeat, &cycleEndBeat) == noErr)
			return true;
	}
	return false;
}

//------------------------------------------------------------------------
FORCE_INLINE void AudioIO::remoteIOPropertyChanged (AudioUnitPropertyID inID,
                                                    AudioUnitScope inScope,
                                                    AudioUnitElement inElement)
{
	if (inID == kAudioUnitProperty_IsInterAppConnected)
	{
		bool wasConnected = interAppAudioConnected;
		updateInterAppAudioConnectionState ();
		if (wasConnected != interAppAudioConnected)
		{
			stop ();
			start ();
		}
	}
}

//------------------------------------------------------------------------
FORCE_INLINE OSStatus AudioIO::renderCallback (AudioUnitRenderActionFlags* ioActionFlags,
                                               const AudioTimeStamp* inTimeStamp,
                                               UInt32 inBusNumber, UInt32 inNumberFrames,
                                               AudioBufferList* ioData)
{
	if (ioData->mNumberBuffers == ioBufferList->mNumberBuffers)
	{
		for (uint32 i = 0; i < ioData->mNumberBuffers; i++)
		{
			memcpy (ioData->mBuffers[i].mData, ioBufferList->mBuffers[i].mData,
			        ioData->mBuffers[i].mDataByteSize);
		}
		bool outputIsSilence =
		    ioActionFlags ? *ioActionFlags & kAudioUnitRenderAction_OutputIsSilence : false;
		for (auto processor : audioProcessors)
		{
			outputIsSilence = false;
			processor->process (inTimeStamp, inBusNumber, inNumberFrames, ioData, outputIsSilence,
			                    this);
		}
		if (ioActionFlags)
		{
			*ioActionFlags = outputIsSilence ? kAudioUnitRenderAction_OutputIsSilence : 0;
		}
	}
	return noErr;
}

//------------------------------------------------------------------------
FORCE_INLINE OSStatus AudioIO::inputCallback (AudioUnitRenderActionFlags* ioActionFlags,
                                              const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber,
                                              UInt32 inNumberFrames, AudioBufferList* ioData)
{
	OSStatus status = AudioUnitRender (remoteIO, ioActionFlags, inTimeStamp, inBusNumber,
	                                   inNumberFrames, ioBufferList);
	return status;
}

//------------------------------------------------------------------------
FORCE_INLINE void AudioIO::midiEventCallback (UInt32 inStatus, UInt32 inData1, UInt32 inData2,
                                              UInt32 inOffsetSampleFrame)
{
	for (auto processor : audioProcessors)
	{
		processor->onMIDIEvent (inStatus, inData1, inData2, inOffsetSampleFrame, true);
	}
}

//------------------------------------------------------------------------
OSStatus AudioIO::inputCallbackStatic (void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags,
                                       const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber,
                                       UInt32 inNumberFrames, AudioBufferList* ioData)
{
	AudioIO* io = (AudioIO*)inRefCon;
	return io->inputCallback (ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
}

//------------------------------------------------------------------------
OSStatus AudioIO::renderCallbackStatic (void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags,
                                        const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber,
                                        UInt32 inNumberFrames, AudioBufferList* ioData)
{
	AudioIO* io = (AudioIO*)inRefCon;
	return io->renderCallback (ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
}

//------------------------------------------------------------------------
void AudioIO::propertyChangeStatic (void* inRefCon, AudioUnit inUnit, AudioUnitPropertyID inID,
                                    AudioUnitScope inScope, AudioUnitElement inElement)
{
	AudioIO* audioIO = (AudioIO*)inRefCon;
	audioIO->remoteIOPropertyChanged (inID, inScope, inElement);
}

//------------------------------------------------------------------------
void AudioIO::midiEventCallbackStatic (void* inRefCon, UInt32 inStatus, UInt32 inData1,
                                       UInt32 inData2, UInt32 inOffsetSampleFrame)
{
	AudioIO* audioIO = (AudioIO*)inRefCon;
	audioIO->midiEventCallback (inStatus, inData1, inData2, inOffsetSampleFrame);
}
}
}
}
