/**********************************************************************

   Audacity - A Digital Audio Editor
   Copyright 1999-2010 Audacity Team
   Michael Chinen

******************************************************************/


#include "DeviceManager.h"

#include <wx/log.h>
#include <thread>



#include "portaudio.h"
#ifdef __WXMSW__
#include "pa_win_wasapi.h"
#endif

#ifdef USE_PORTMIXER
#include "portmixer.h"
#endif

#include "AudioIOBase.h"

#include "DeviceChange.h" // for HAVE_DEVICE_CHANGE

DeviceManager DeviceManager::dm;

/// Gets the singleton instance
DeviceManager* DeviceManager::Instance()
{
   return &dm;
}

const std::vector<DeviceSourceMap> &DeviceManager::GetInputDeviceMaps()
{
   if (!m_inited)
      Init();
   return mInputDeviceSourceMaps;
}
const std::vector<DeviceSourceMap> &DeviceManager::GetOutputDeviceMaps()
{
   if (!m_inited)
      Init();
   return mOutputDeviceSourceMaps;
}


wxString MakeDeviceSourceString(const DeviceSourceMap *map)
{
   wxString ret;
   ret = map->deviceString;
   if (map->totalSources > 1)
      ret += wxT(": ") + map->sourceString;

   return ret;
}

DeviceSourceMap* DeviceManager::GetDefaultDevice(int hostIndex, int isInput)
{
   if (hostIndex < 0 || hostIndex >= Pa_GetHostApiCount()) {
      return NULL;
   }

   const struct PaHostApiInfo *apiinfo = Pa_GetHostApiInfo(hostIndex);   // get info on API
   std::vector<DeviceSourceMap> & maps = isInput ? mInputDeviceSourceMaps : mOutputDeviceSourceMaps;
   size_t i;
   int targetDevice = isInput ? apiinfo->defaultInputDevice : apiinfo->defaultOutputDevice;

   for (i = 0; i < maps.size(); i++) {
      if (maps[i].deviceIndex == targetDevice)
         return &maps[i];
   }

   wxLogDebug(wxT("GetDefaultDevice() no default device"));
   return NULL;
}

DeviceSourceMap* DeviceManager::GetDefaultOutputDevice(int hostIndex)
{
   return GetDefaultDevice(hostIndex, 0);
}
DeviceSourceMap* DeviceManager::GetDefaultInputDevice(int hostIndex)
{
   return GetDefaultDevice(hostIndex, 1);
}

//--------------- Device Enumeration --------------------------

//Port Audio requires we open the stream with a callback or a lot of devices will fail
//as this means open in blocking mode, so we use a dummy one.
static int DummyPaStreamCallback(
    const void *WXUNUSED(input), void * WXUNUSED(output),
    unsigned long WXUNUSED(frameCount),
    const PaStreamCallbackTimeInfo* WXUNUSED(timeInfo),
    PaStreamCallbackFlags WXUNUSED(statusFlags),
    void *WXUNUSED(userData) )
{
   return 0;
}

static void FillHostDeviceInfo(DeviceSourceMap *map, const PaDeviceInfo *info, int deviceIndex, int isInput)
{
   wxString hostapiName = wxSafeConvertMB2WX(Pa_GetHostApiInfo(info->hostApi)->name);
   wxString infoName = wxSafeConvertMB2WX(info->name);

   map->deviceIndex  = deviceIndex;
   map->hostIndex    = info->hostApi;
   map->deviceString = infoName;
   map->hostString   = hostapiName;
   map->numChannels  = isInput ? info->maxInputChannels : info->maxOutputChannels;
}

static void AddSourcesFromStream(int deviceIndex, const PaDeviceInfo *info, std::vector<DeviceSourceMap> *maps, PaStream *stream)
{
#ifdef USE_PORTMIXER
   int i;
#endif
   DeviceSourceMap map;

   map.sourceIndex  = -1;
   map.totalSources = 0;
   // Only inputs have sources, so we call FillHostDeviceInfo with a 1 to indicate this
   FillHostDeviceInfo(&map, info, deviceIndex, 1);

#ifdef USE_PORTMIXER
   PxMixer *portMixer = Px_OpenMixer(stream, deviceIndex, -1, 0);
   if (!portMixer) {
      maps->push_back(map);
      return;
   }

   //if there is only one source, we don't need to concatenate the source
   //or enumerate, because it is something meaningless like 'master'
   //(as opposed to 'mic in' or 'line in'), and the user doesn't have any choice.
   //note that some devices have no input sources at all but are still valid.
   //the behavior we do is the same for 0 and 1 source cases.
   map.totalSources = Px_GetNumInputSources(portMixer);
#endif

   if (map.totalSources <= 1) {
      map.sourceIndex = 0;
      maps->push_back(map);
   }
#ifdef USE_PORTMIXER
     else {
      //open up a stream with the device so portmixer can get the info out of it.
      for (i = 0; i < map.totalSources; i++) {
         map.sourceIndex  = i;
         map.sourceString = wxString(wxSafeConvertMB2WX(Px_GetInputSourceName(portMixer, i)));
         maps->push_back(map);
      }
   }
   Px_CloseMixer(portMixer);
#endif
}

static bool IsInputDeviceAMapperDevice(const PaDeviceInfo *info)
{
   // For Windows only, portaudio returns the default mapper object
   // as the first index after a NEW hostApi index is detected (true for MME and DS)
   // this is a bit of a hack, but there's no other way to find out which device is a mapper,
   // I've looked at string comparisons, but if the system is in a different language this breaks.
#ifdef __WXMSW__
   static int lastHostApiTypeId = -1;
   int hostApiTypeId = Pa_GetHostApiInfo(info->hostApi)->type;
   if(hostApiTypeId != lastHostApiTypeId &&
      (hostApiTypeId == paMME || hostApiTypeId == paDirectSound)) {
      lastHostApiTypeId = hostApiTypeId;
      return true;
   }
#endif

   return false;
}

static void AddSources(int deviceIndex, int rate, std::vector<DeviceSourceMap> *maps, int isInput)
{
   int error = 0;
   DeviceSourceMap map;
   const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceIndex);

   // This tries to open the device with the samplerate worked out above, which
   // will be the highest available for play and record on the device, or
   // 44.1kHz if the info cannot be fetched.

   PaStream *stream = NULL;

   PaStreamParameters parameters;

   parameters.device = deviceIndex;
   parameters.sampleFormat = paFloat32;
   parameters.hostApiSpecificStreamInfo = NULL;
   parameters.channelCount = 1;

   // If the device is for input, open a stream so we can use portmixer to query
   // the number of inputs.  We skip this for outputs because there are no 'sources'
   // and some platforms (e.g. XP) have the same device for input and output, (while
   // Vista/Win7 separate these into two devices with the same names (but different
   // portaudio indices)
   // Also, for mapper devices we don't want to keep any sources, so check for it here
   if (isInput && !IsInputDeviceAMapperDevice(info)) {
      if (info)
         parameters.suggestedLatency = info->defaultLowInputLatency;
      else
         parameters.suggestedLatency = 10.0;

      error = Pa_OpenStream(&stream,
                            &parameters,
                            NULL,
                            rate, paFramesPerBufferUnspecified,
                            paClipOff | paDitherOff,
                            DummyPaStreamCallback, NULL);
   }

   if (stream && !error) {
      AddSourcesFromStream(deviceIndex, info, maps, stream);
      Pa_CloseStream(stream);
   } else {
      map.sourceIndex  = -1;
      map.totalSources = 0;
      FillHostDeviceInfo(&map, info, deviceIndex, isInput);
      maps->push_back(map);
   }

   if(error) {
      wxLogDebug(wxT("PortAudio stream error creating device list: ") +
                 map.hostString + wxT(":") + map.deviceString + wxT(": ") +
                 wxString(wxSafeConvertMB2WX(Pa_GetErrorText((PaError)error))));
   }
}

/// Gets a NEW list of devices by terminating and restarting portaudio
/// Assumes that DeviceManager is only used on the main thread.
void DeviceManager::Rescan()
{
   // get rid of the previous scan info
   this->mInputDeviceSourceMaps.clear();
   this->mOutputDeviceSourceMaps.clear();

   // if we are doing a second scan then restart portaudio to get NEW devices
   if (m_inited) {
      // check to see if there is a stream open - can happen if monitoring,
      // but otherwise Rescan() should not be available to the user.
      auto gAudioIO = AudioIOBase::Get();
      if (gAudioIO) {
         if (gAudioIO->IsMonitoring())
         {
            using namespace std::chrono;
            gAudioIO->StopStream();
            while (gAudioIO->IsBusy())
               std::this_thread::sleep_for(100ms);
         }
      }

      // restart portaudio - this updates the device list
      // FIXME: TRAP_ERR restarting PortAudio
      Pa_Terminate();
      Pa_Initialize();
   }

   // FIXME: TRAP_ERR PaErrorCode not handled in ReScan()
   int nDevices = Pa_GetDeviceCount();

   //The hierarchy for devices is Host/device/source.
   //Some newer systems aggregate this.
   //So we need to call port mixer for every device to get the sources
   for (int i = 0; i < nDevices; i++) {
      const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
      if (info->maxOutputChannels > 0) {
         AddSources(i, info->defaultSampleRate, &mOutputDeviceSourceMaps, 0);
      }

      if (info->maxInputChannels > 0) {
         AddSources(i, info->defaultSampleRate, &mInputDeviceSourceMaps, 1);
      }
   }

   // If this was not an initial scan update each device toolbar.
   if ( m_inited )
      Publish(DeviceChangeMessage::Rescan);

   m_inited = true;
   mRescanTime = std::chrono::steady_clock::now();
}


std::chrono::duration<float> DeviceManager::GetTimeSinceRescan() {
   auto now = std::chrono::steady_clock::now();
   auto dur = std::chrono::duration_cast<std::chrono::duration<float>>(now - mRescanTime);
   return dur;
}


//private constructor - Singleton.
DeviceManager::DeviceManager()
#if defined(EXPERIMENTAL_DEVICE_CHANGE_HANDLER)
#if defined(HAVE_DEVICE_CHANGE)
:  DeviceChangeHandler()
#endif
#endif
{
   m_inited = false;
   mRescanTime = std::chrono::steady_clock::now();
}

DeviceManager::~DeviceManager()
{

}

void DeviceManager::Init()
{
    Rescan();

#if defined(EXPERIMENTAL_DEVICE_CHANGE_HANDLER)
#if defined(HAVE_DEVICE_CHANGE)
   DeviceChangeHandler::Enable(true);
#endif
#endif
}

#if defined(EXPERIMENTAL_DEVICE_CHANGE_HANDLER)
#if defined(HAVE_DEVICE_CHANGE)
void DeviceManager::DeviceChangeNotification()
{
   Rescan();
   return;
}
#endif
#endif
