//	PortAudioDevice.cpp - A PortAudio subclass of AudioDevice.
//  ----------------------------------------------------------------------------
//	This file is part of 'NiallsAVLib', base code for any kind of audiovisual
//	apps.
//	Copyright (C) 2012  Niall Moody
//	
//	This program is free software: you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or
//	(at your option) any later version.
//
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program.  If not, see <http://www.gnu.org/licenses/>.
//	----------------------------------------------------------------------------

#include "PortAudioDevice.h"
#include "DebugHeaders.h"
#include "UTF8File.h"

#include <stdint.h>
#include <cassert>
#include <sstream>

using std::wstring;
using std::string;

//------------------------------------------------------------------------------
PortAudioDevice::PortAudioDevice():
stream(0),
callback(0),
inputDevice(L"Default"),
outputDevice(L"Default"),
numInputs(0),
numOutputs(2),
samplerate(44100.0),
isRunning(false),
initialised(false)
{
	int i;

	if(Pa_Initialize() == paNoError)
	{
		const int numAPIs = Pa_GetHostApiCount();

		initialised = true;

		for(i=0;i<numAPIs;++i)
		{
			const PaHostApiInfo *apiInfo = Pa_GetHostApiInfo(i);

			if(apiInfo->deviceCount > 0)
			{
				std::wstring tempstr;

				UTF8File::charToWstring(apiInfo->name, tempstr);
				apiList.push_back(tempstr);
			}
		}
	}
	else
		DebugStatement(L"PortAudioDevice Error: Could not initialise.");

#ifdef WIN32
	setAPI(L"DirectSound");
#elif defined(LINUX)
	setAPI(L"ALSA");
#elif defined(OSX)
	setAPI(L"CoreAudio");
#endif
}

//------------------------------------------------------------------------------
PortAudioDevice::~PortAudioDevice()
{
	if(isRunning)
		stop();
	if(initialised)
		Pa_Terminate();
}

//------------------------------------------------------------------------------
void PortAudioDevice::setCallback(AudioDeviceCallback *val)
{
	callback = val;
}

//------------------------------------------------------------------------------
void PortAudioDevice::setNumChannels(int inputs, int outputs)
{
	numInputs = inputs;
	numOutputs = outputs;
}

//------------------------------------------------------------------------------
void PortAudioDevice::setSamplerate(double rate)
{
	samplerate = rate;
}

//------------------------------------------------------------------------------
void PortAudioDevice::setDevices(const std::wstring& inDevice,
								 const std::wstring& outDevice)
{
	inputDevice = inDevice;
	outputDevice = outDevice;
}

//------------------------------------------------------------------------------
void PortAudioDevice::setAPI(const wstring& val)
{
	if((val == L"DirectSound") || (val == L"Windows DirectSound"))
	{
		api = paDirectSound;
		apiName = L"Windows DirectSound";
	}
	else if(val == L"ASIO")
	{
		api = paASIO;
		apiName = L"ASIO";
	}
	else if(val == L"MME")
	{
		api = paMME;
		apiName = L"MME";
	}
	else if((val == L"WDM") || (val == L"Windows WDM-KS"))
	{
		api = paWDMKS;
		apiName = L"Windows WDM-KS";
	}
	else if((val == L"WASAPI") || (val == L"Windows WASAPI"))
	{
		api = paWASAPI;
		apiName = L"Windows WASAPI";
	}
	else if(val == L"ALSA")
	{
		api = paALSA;
		apiName = L"ALSA";
	}
	else if((val == L"JACK") || (val == L"JACK Audio Connection Kit"))
	{
		api = paJACK;
		apiName = L"JACK Audio Connection Kit";
	}
	else if(val == L"OSS")
	{
		api = paOSS;
		apiName = L"OSS";
	}
	else if((val == L"CoreAudio") || (val == L"Core Audio"))
	{
		api = paCoreAudio;
		apiName = L"Core Audio";
	}
	else
	{
#ifdef WIN32
		api = paDirectSound;
		apiName = L"Windows DirectSound";
#elif defined(LINUX)
		api = paALSA;
		apiName = L"ALSA";
#elif defined(OSX)
		api = paCoreAudio;
		apiName = L"Core Audio";
#endif
	}

	DebugStatement(L"PortAudioDevice: API set to " << apiName);

	//Populate inputList & outputList.
	{
		int i;
		PaHostApiIndex apiIndex = Pa_HostApiTypeIdToHostApiIndex(api);
		const PaHostApiInfo *apiInfo = Pa_GetHostApiInfo(apiIndex);

		if(!apiInfo)
		{
			DebugStatement(L"PortAudioDevice: Unable to set	API to " << apiName);
			return;
		}

		inputList.clear();
		outputList.clear();
		inputChannels.clear();
		outputChannels.clear();

		DebugStatement(L"PortAudioDevice: Devices for " << apiName << " API follow");
		for(i=0;i<apiInfo->deviceCount;++i)
		{
			PaDeviceIndex deviceIndex = Pa_HostApiDeviceIndexToDeviceIndex(apiIndex, i);
			const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(deviceIndex);

			if(deviceInfo->maxInputChannels > 0)
			{
				std::wstring tempName;

				UTF8File::charToWstring(deviceInfo->name, tempName);
				inputList.push_back(tempName);
				inputChannels.push_back(deviceInfo->maxInputChannels);
				DebugStatement(L"\tInput: " << inputList.back() << L":channels=" << inputChannels.back() << L":id=" << deviceIndex);
			}
			if(deviceInfo->maxOutputChannels > 0)
			{
				std::wstring tempName;

				UTF8File::charToWstring(deviceInfo->name, tempName);
				outputList.push_back(tempName);
				outputChannels.push_back(deviceInfo->maxOutputChannels);
				DebugStatement(L"\tOutput: " << outputList.back() << L":channels=" << outputChannels.back() << L":id=" << deviceIndex);
			}
		}
	}
}

//------------------------------------------------------------------------------
bool PortAudioDevice::start()
{
	bool retval = false;

	if(!initialised)
		return retval;

	if(!isRunning)
	{
		//const PaHostApiInfo *apiInfo;
		PaStreamParameters inputParams, outputParams;
		PaError err;
		const PaDeviceInfo *devInfo;
		PaHostApiIndex apiIndex = Pa_HostApiTypeIdToHostApiIndex(api);
		PaDeviceIndex devIndex;

		if(apiIndex == paHostApiNotFound)
			return false;
		else
		{
			switch(api)
			{
				case paDirectSound:
					DebugStatement(L"PortAudioDevice: Using DirectSound API.");
					break;
				case paASIO:
					DebugStatement(L"PortAudioDevice: Using ASIO API.");
					break;
				case paMME:
					DebugStatement(L"PortAudioDevice: Using MME API.");
					break;
				case paWDMKS:
					DebugStatement(L"PortAudioDevice: Using WDM API.");
					break;
				case paWASAPI:
					DebugStatement(L"PortAudioDevice: Using WASAPI API.");
					break;
				case paALSA:
					DebugStatement(L"PortAudioDevice: Using ALSA API.");
					break;
				case paJACK:
					DebugStatement(L"PortAudioDevice: Using JACK API.");
					break;
				case paOSS:
					DebugStatement(L"PortAudioDevice: Using OSS API.");
					break;
				case paCoreAudio:
					DebugStatement(L"PortAudioDevice: Using CoreAudio API.");
					break;
				default:
					DebugStatement(L"PortAudioDevice: Using unknown API. Type ID = " << api);
			}
		}

		//apiInfo = Pa_GetHostApiInfo(apiIndex);

		devIndex = getDeviceIndexFromName(inputDevice, false);
		devInfo = Pa_GetDeviceInfo(devIndex);

		inputParams.device = devIndex;
		inputParams.channelCount = numInputs;
		inputParams.sampleFormat = paFloat32;
		if(devInfo)
			inputParams.suggestedLatency = devInfo->defaultLowInputLatency;
		else
			inputParams.suggestedLatency = 0.0;
		inputParams.hostApiSpecificStreamInfo = 0;

		DebugStatement(L"PortAudioDevice: Input device index = " << inputParams.device);
		
		devIndex = getDeviceIndexFromName(outputDevice, true);
		devInfo = Pa_GetDeviceInfo(devIndex);

		outputParams.device = devIndex;
		outputParams.channelCount = numOutputs;
		outputParams.sampleFormat = paFloat32;
		if(devInfo)
			outputParams.suggestedLatency = devInfo->defaultLowOutputLatency;
		else
			outputParams.suggestedLatency = 0.0;
		outputParams.hostApiSpecificStreamInfo = 0;

		DebugStatement(L"PortAudioDevice: Output device index = " << outputParams.device);

		if(Pa_IsFormatSupported(NULL, &outputParams, samplerate) == paInvalidSampleRate)
		{
			DebugStatement(L"44100Hz samplerate not supported, using 48000Hz.");
			samplerate = 48000.0; //This is obviously a nasty hacky way of doing things.

			if(Pa_IsFormatSupported(NULL, &outputParams, samplerate) == paInvalidSampleRate)
				DebugStatement(L"48000Hz not supported either?");
		}

		err = Pa_OpenStream(&stream,
							(numInputs < 1) ? 0 : &inputParams,
							(numOutputs < 1) ? 0 : &outputParams,
							samplerate,
							0,
							paNoFlag,
							portAudioCallback,
							(void *)callback);
		if(err != paNoError)
		{
			string tempstr;
			wstring tempstr2;
			DebugStatement(L"PortAudioDevice Error: Could not open stream.");

			tempstr = Pa_GetErrorText(err);
			UTF8File::charToWstring(tempstr, tempstr2);
			DebugStatement(tempstr2);
		}
		else if(Pa_StartStream(stream) == paNoError)
		{
			DebugStatement(L"PortAudioDevice: Stream started successfully");

			isRunning = true;
			retval = true;
		}
		else //What on earth is this code doing? We'll never reach here.
		{
			DebugStatement(L"PortAudioDevice Error: Could not start stream.");
			Pa_CloseStream(stream);
		}
	}

	return retval;
}

//------------------------------------------------------------------------------
void PortAudioDevice::stop()
{
	if(!initialised)
		return;

	if(isRunning)
	{
		Pa_StopStream(stream);
		Pa_CloseStream(stream);
		isRunning = false;
	}
}

//------------------------------------------------------------------------------
int PortAudioDevice::portAudioCallback(const void *input,
									   void *output,
									   unsigned long frameCount,
									   const PaStreamCallbackTimeInfo *timeInfo,
									   PaStreamCallbackFlags statusFlags,
									   void *userData)
{
	unsigned long i;
	AudioDeviceCallback *callback = (AudioDeviceCallback *)userData;

	//Because it seems to be filled with the input(/nonsense), we need to blank
	//the output buffer.
	for(i=0;i<(frameCount*2);++i)
		((float *)output)[i] = 0.0f;

	if(callback)
		callback->getAudio((float *)input, (float *)output, frameCount);

	return paContinue;
}

//------------------------------------------------------------------------------
PaDeviceIndex PortAudioDevice::getDeviceIndexFromName(const std::wstring& device,
													  bool isOutput)
{
	std::string narrowDevice;
	PaDeviceIndex retval = paNoDevice;
	PaHostApiIndex apiIndex = Pa_HostApiTypeIdToHostApiIndex(api);
	const PaHostApiInfo *apiInfo = Pa_GetHostApiInfo(apiIndex);

	if(device != L"Default")
	{
		int i;

		UTF8File::wstringToChar(device, narrowDevice);

		for(i=0;i<apiInfo->deviceCount;++i)
		{
			PaDeviceIndex deviceIndex = Pa_HostApiDeviceIndexToDeviceIndex(apiIndex, i);
			const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(deviceIndex);

			if(narrowDevice == deviceInfo->name)
			{
				retval = deviceIndex;
				break;
			}
		}
	}

	if(retval == paNoDevice)
	{
		if(isOutput)
			retval = apiInfo->defaultOutputDevice;
		else
			retval = apiInfo->defaultInputDevice;
	}

	return retval;
}
