/* GStreamer
 * Copyright (C) 2025 Seungha Yang <seungha@centricular.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gstwasapi2enumerator.h"
#include "gstwasapi2activator.h"
#include <mutex>
#include <condition_variable>
#include <wrl.h>
#include <functiondiscoverykeys_devpkey.h>
#include <string>
#include <atomic>

/* *INDENT-OFF* */
using namespace Microsoft::WRL;

#ifndef GST_DISABLE_GST_DEBUG
#define GST_CAT_DEFAULT ensure_debug_category()
static GstDebugCategory *
ensure_debug_category (void)
{
  static GstDebugCategory *cat = nullptr;

  GST_WASAPI2_CALL_ONCE_BEGIN {
    cat = _gst_debug_category_new ("wasapi2enumerator", 0, "wasapi2enumerator");
  } GST_WASAPI2_CALL_ONCE_END;

  return cat;
}
#endif

static void gst_wasapi2_on_device_updated (GstWasapi2Enumerator * object);

static std::string
device_state_to_string (DWORD state)
{
  std::string ret;
  bool is_first = true;
  if ((state & DEVICE_STATE_ACTIVE) == DEVICE_STATE_ACTIVE) {
    if (!is_first)
      ret += "|";
    ret += "ACTIVE";
    is_first = false;
  }

  if ((state & DEVICE_STATE_DISABLED) == DEVICE_STATE_DISABLED) {
    if (!is_first)
      ret += "|";
    ret += "DISABLED";
    is_first = false;
  }

  if ((state & DEVICE_STATE_NOTPRESENT) == DEVICE_STATE_NOTPRESENT) {
    if (!is_first)
      ret += "|";
    ret += "NOTPRESENT";
    is_first = false;
  }

  if ((state & DEVICE_STATE_UNPLUGGED) == DEVICE_STATE_UNPLUGGED) {
    if (!is_first)
      ret += "|";
    ret += "UNPLUGGED";
    is_first = false;
  }

  return ret;
}

/* IMMNotificationClient implementation */
class IWasapi2NotificationClient : public IMMNotificationClient
{
public:
  static void
  CreateInstance (GstWasapi2Enumerator * object, IMMNotificationClient ** client)
  {
    auto self = new IWasapi2NotificationClient ();

    g_weak_ref_set (&self->obj_, object);

    *client = (IMMNotificationClient *) self;
  }

  /* IUnknown */
  STDMETHODIMP
  QueryInterface (REFIID riid, void ** object)
  {
    if (!object)
      return E_POINTER;

    if (riid == IID_IUnknown) {
      *object = static_cast<IUnknown *> (this);
    } else if (riid == __uuidof(IMMNotificationClient)) {
      *object = static_cast<IMMNotificationClient *> (this);
    } else {
      *object = nullptr;
      return E_NOINTERFACE;
    }

    AddRef ();

    return S_OK;
  }

  STDMETHODIMP_ (ULONG)
  AddRef (void)
  {
    return InterlockedIncrement (&ref_count_);
  }

  STDMETHODIMP_ (ULONG)
  Release (void)
  {
    ULONG ref_count;

    GST_TRACE ("%p, %d", this, (guint) ref_count_);
    ref_count = InterlockedDecrement (&ref_count_);

    if (ref_count == 0) {
      GST_TRACE ("Delete instance %p", this);
      delete this;
    }

    return ref_count;
  }

  /* IMMNotificationClient */
  STDMETHODIMP
  OnDeviceStateChanged (LPCWSTR device_id, DWORD new_state)
  {
    auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
    if (!object)
      return S_OK;

    auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
          -1, nullptr, nullptr, nullptr);
    auto state = device_state_to_string (new_state);
    GST_LOG ("%s, %s (0x%x)", id, state.c_str (), (guint) new_state);
    g_free (id);

    gst_wasapi2_on_device_updated (object);
    gst_object_unref (object);

    return S_OK;
  }

  STDMETHODIMP
  OnDeviceAdded (LPCWSTR device_id)
  {
    auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
    if (!object)
      return S_OK;

    auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
          -1, nullptr, nullptr, nullptr);
    GST_LOG ("%s", id);
    g_free (id);

    gst_wasapi2_on_device_updated (object);
    gst_object_unref (object);

    return S_OK;
  }

  STDMETHODIMP
  OnDeviceRemoved (LPCWSTR device_id)
  {
    auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
    if (!object)
      return S_OK;

    auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
          -1, nullptr, nullptr, nullptr);
    GST_LOG ("%s", id);
    g_free (id);

    gst_wasapi2_on_device_updated (object);
    gst_object_unref (object);

    return S_OK;
  }

  STDMETHODIMP
  OnDefaultDeviceChanged (EDataFlow flow, ERole role, LPCWSTR device_id)
  {
    auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
    if (!object)
      return S_OK;

    auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
          -1, nullptr, nullptr, nullptr);
    GST_LOG ("%s, flow: %s, role: %s", id,
        gst_wasapi2_data_flow_to_string (flow),
        gst_wasapi2_role_to_string (role));
    g_free (id);

    gst_wasapi2_on_device_updated (object);
    gst_object_unref (object);

    return S_OK;
  }

  STDMETHODIMP
  OnPropertyValueChanged (LPCWSTR device_id, const PROPERTYKEY key)
  {
    return S_OK;
  }

private:
  IWasapi2NotificationClient ()
  {
    g_weak_ref_init (&obj_, nullptr);
  }

  virtual ~IWasapi2NotificationClient ()
  {
    g_weak_ref_clear (&obj_);
  }

private:
  ULONG ref_count_ = 1;
  GWeakRef obj_;
};

enum
{
  PROP_0,
  PROP_ENUMERATOR,
};

enum
{
  SIGNAL_UPDATED,
  SIGNAL_LAST,
};

static guint wasapi2_device_signals[SIGNAL_LAST] = { };

struct GstWasapi2EnumeratorPrivate
{
  GstWasapi2EnumeratorPrivate ()
  {
    device_list = g_ptr_array_new_with_free_func ((GDestroyNotify)
        gst_wasapi2_enumerator_entry_free);
    endpoint_formats = g_ptr_array_new_with_free_func ((GDestroyNotify)
        gst_wasapi2_free_wfx);
  }

  ~GstWasapi2EnumeratorPrivate ()
  {
    g_ptr_array_unref (device_list);
    g_ptr_array_unref (endpoint_formats);
  }

  ComPtr<IMMDeviceEnumerator> handle;
  std::mutex lock;
  std::condition_variable cond;

  ComPtr<IMMNotificationClient> client;
  Wasapi2ActivationHandler *capture_activator = nullptr;
  Wasapi2ActivationHandler *render_activator = nullptr;
  std::atomic<int> notify_count = { 0 };
  GPtrArray *device_list;
  GPtrArray *endpoint_formats;

  void ClearCOM ()
  {
    if (capture_activator) {
      capture_activator->GetClient (nullptr, INFINITE);
      capture_activator->Release ();
    }

    if (render_activator) {
      render_activator->GetClient (nullptr, INFINITE);
      render_activator->Release ();
    }

    if (client && handle)
      handle->UnregisterEndpointNotificationCallback (client.Get ());

    client = nullptr;
    handle = nullptr;
  }
};
/* *INDENT-ON* */

struct _GstWasapi2Enumerator
{
  GstObject parent;

  GstWasapi2EnumeratorPrivate *priv;

  GThread *thread;
  GMainContext *context;
  GMainLoop *loop;
};

static void gst_wasapi2_enumerator_finalize (GObject * object);

#define gst_wasapi2_enumerator_parent_class parent_class
G_DEFINE_TYPE (GstWasapi2Enumerator, gst_wasapi2_enumerator, GST_TYPE_OBJECT);

static void
gst_wasapi2_enumerator_class_init (GstWasapi2EnumeratorClass * klass)
{
  auto object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = gst_wasapi2_enumerator_finalize;

  wasapi2_device_signals[SIGNAL_UPDATED] =
      g_signal_new_class_handler ("updated", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, nullptr, nullptr, nullptr, nullptr, G_TYPE_NONE, 0);
}

static void
gst_wasapi2_enumerator_init (GstWasapi2Enumerator * self)
{
  self->priv = new GstWasapi2EnumeratorPrivate ();
  self->context = g_main_context_new ();
  self->loop = g_main_loop_new (self->context, FALSE);
}

static void
gst_wasapi2_enumerator_finalize (GObject * object)
{
  auto self = GST_WASAPI2_ENUMERATOR (object);

  g_main_loop_quit (self->loop);
  g_thread_join (self->thread);
  g_main_loop_unref (self->loop);
  g_main_context_unref (self->context);

  delete self->priv;

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_wasapi2_on_device_updated (GstWasapi2Enumerator * object)
{
  /* *INDENT-OFF* */
  auto priv = object->priv;

  auto count = priv->notify_count.fetch_add (1);
  GST_LOG ("notify count before scheduling %d", count);

  auto source = g_timeout_source_new (100);
  g_source_set_callback (source,
      [] (gpointer obj) -> gboolean {
        auto self = GST_WASAPI2_ENUMERATOR (obj);
        auto priv = self->priv;
        auto count = priv->notify_count.fetch_sub (1);
        GST_LOG ("scheduled notify count %d", count);
        if (count == 1)
          g_signal_emit (obj, wasapi2_device_signals[SIGNAL_UPDATED], 0);
        return G_SOURCE_REMOVE;
      },
      gst_object_ref (object), (GDestroyNotify) gst_object_unref);

  g_source_attach (source, object->context);
  g_source_unref (source);
  /* *INDENT-ON* */
}

static gpointer
gst_wasapi2_enumerator_thread_func (GstWasapi2Enumerator * self)
{
  auto priv = self->priv;

  CoInitializeEx (nullptr, COINIT_MULTITHREADED);

  g_main_context_push_thread_default (self->context);

  auto idle_source = g_idle_source_new ();
  /* *INDENT-OFF* */
  g_source_set_callback (idle_source,
      [] (gpointer user_data) -> gboolean {
        auto self = (GstWasapi2Enumerator *) user_data;
        auto priv = self->priv;
        std::lock_guard < std::mutex > lk (priv->lock);
        priv->cond.notify_all ();
        return G_SOURCE_REMOVE;
      },
      self, nullptr);
  /* *INDENT-ON* */
  g_source_attach (idle_source, self->context);
  g_source_unref (idle_source);

  auto hr = CoCreateInstance (__uuidof (MMDeviceEnumerator),
      nullptr, CLSCTX_ALL, IID_PPV_ARGS (&priv->handle));
  if (FAILED (hr)) {
    GST_ERROR_OBJECT (self, "Failed to create IMMDeviceEnumerator instance");
    goto run_loop;
  }

  if (gst_wasapi2_can_automatic_stream_routing ()) {
    Wasapi2ActivationHandler::CreateInstance (&priv->capture_activator,
        gst_wasapi2_get_default_device_id_wide (eCapture), nullptr);
    priv->capture_activator->ActivateAsync ();

    Wasapi2ActivationHandler::CreateInstance (&priv->render_activator,
        gst_wasapi2_get_default_device_id_wide (eRender), nullptr);
    priv->render_activator->ActivateAsync ();
  }

run_loop:
  GST_INFO_OBJECT (self, "Starting loop");
  g_main_loop_run (self->loop);
  GST_INFO_OBJECT (self, "Stopped loop");

  priv->ClearCOM ();

  g_main_context_pop_thread_default (self->context);

  CoUninitialize ();

  return nullptr;
}

GstWasapi2Enumerator *
gst_wasapi2_enumerator_new (void)
{
  auto self = (GstWasapi2Enumerator *)
      g_object_new (GST_TYPE_WASAPI2_ENUMERATOR, nullptr);
  gst_object_ref_sink (self);

  auto priv = self->priv;

  {
    std::unique_lock < std::mutex > lk (priv->lock);
    self->thread = g_thread_new ("GstWasapi2Enumerator",
        (GThreadFunc) gst_wasapi2_enumerator_thread_func, self);
    while (!g_main_loop_is_running (self->loop))
      priv->cond.wait (lk);
  }

  if (!priv->handle) {
    gst_object_unref (self);
    return nullptr;
  }

  return self;
}

/* *INDENT-OFF* */
struct ActivateNotificationData
{
  ActivateNotificationData ()
  {
    event = CreateEvent (nullptr, FALSE, FALSE, nullptr);
  }

  ~ActivateNotificationData ()
  {
    CloseHandle (event);
  }

  GstWasapi2Enumerator *self;
  gboolean active;
  HANDLE event;
};
/* *INDENT-ON* */

static gboolean
set_notification_callback (ActivateNotificationData * data)
{
  auto self = data->self;
  auto priv = self->priv;

  if (data->active) {
    if (!priv->client) {
      ComPtr < IMMNotificationClient > client;
      IWasapi2NotificationClient::CreateInstance (self, &client);

      auto hr =
          priv->handle->RegisterEndpointNotificationCallback (client.Get ());
      if (FAILED (hr)) {
        GST_ERROR_OBJECT (self, "Couldn't register callback");
      } else {
        GST_LOG_OBJECT (self, "Registered notification");
        priv->client = client;
      }
    }
  } else if (priv->client) {
    priv->handle->UnregisterEndpointNotificationCallback (priv->client.Get ());
    priv->client = nullptr;
    GST_LOG_OBJECT (self, "Unregistered notification");
  }

  SetEvent (data->event);

  return G_SOURCE_REMOVE;
}

void
gst_wasapi2_enumerator_activate_notification (GstWasapi2Enumerator * object,
    gboolean active)
{
  auto priv = object->priv;

  if (!priv->handle)
    return;

  ActivateNotificationData data;
  data.self = object;
  data.active = active;

  g_main_context_invoke (object->context,
      (GSourceFunc) set_notification_callback, &data);

  WaitForSingleObject (data.event, INFINITE);
}

void
gst_wasapi2_enumerator_entry_free (GstWasapi2EnumeratorEntry * entry)
{
  delete entry;
}

/* *INDENT-OFF* */
struct EnumerateData
{
  EnumerateData ()
  {
    event = CreateEvent (nullptr, FALSE, FALSE, nullptr);
  }

  ~EnumerateData ()
  {
    CloseHandle (event);
  }

  GstWasapi2Enumerator *self;
  GPtrArray *device_list;
  HANDLE event;
};
/* *INDENT-ON* */

static GstWasapi2EnumeratorEntry *
gst_wasapi2_enumerator_build_entry (GstWasapi2Enumerator * self,
    GstCaps * caps, EDataFlow flow, gboolean is_default,
    gchar * device_id, gchar * device_name,
    gchar * actual_device_id, gchar * actual_device_name,
    GstWasapi2DeviceProps * device_props)
{
  auto entry = new GstWasapi2EnumeratorEntry ();

  entry->device_id = device_id;
  entry->device_name = device_name;
  entry->caps = caps;
  entry->flow = flow;
  entry->is_default = is_default;
  if (actual_device_id)
    entry->actual_device_id = actual_device_id;
  if (actual_device_name)
    entry->actual_device_name = actual_device_name;

  if (device_props) {
    entry->device_props.form_factor = device_props->form_factor;
    entry->device_props.enumerator_name = device_props->enumerator_name;
  }

  GST_LOG_OBJECT (self, "Adding entry %s (%s), flow %d, caps %" GST_PTR_FORMAT,
      device_id, device_name, flow, caps);
  g_free (device_id);
  g_free (device_name);
  g_free (actual_device_id);
  g_free (actual_device_name);

  return entry;
}

static void
gst_wasapi2_enumerator_probe_props (IPropertyStore * store,
    GstWasapi2DeviceProps * props)
{
  PROPVARIANT var;
  PropVariantInit (&var);

  auto hr = store->GetValue (PKEY_AudioEndpoint_FormFactor, &var);
  if (SUCCEEDED (hr) && var.vt == VT_UI4)
    props->form_factor = (EndpointFormFactor) var.ulVal;

  PropVariantClear (&var);

  hr = store->GetValue (PKEY_Device_EnumeratorName, &var);
  if (SUCCEEDED (hr) && var.vt == VT_LPWSTR) {
    auto name = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
        -1, nullptr, nullptr, nullptr);
    props->enumerator_name = name;
    g_free (name);
  }

  PropVariantClear (&var);
}

static void
get_default_device (GstWasapi2Enumerator * self, EDataFlow flow,
    IMMDevice ** device, IPropertyStore ** prop, gchar ** actual_device_id,
    gchar ** actual_device_name)
{
  auto priv = self->priv;
  ComPtr < IMMDevice > rst_device;
  ComPtr < IPropertyStore > rst_prop;

  *actual_device_id = nullptr;
  *actual_device_name = nullptr;

  auto hr = priv->handle->GetDefaultAudioEndpoint (flow,
      eConsole, &rst_device);
  if (FAILED (hr))
    return;

  hr = rst_device->OpenPropertyStore (STGM_READ, &rst_prop);
  if (FAILED (hr))
    return;

  LPWSTR wid = nullptr;
  hr = rst_device->GetId (&wid);
  if (!gst_wasapi2_result (hr))
    return;

  *actual_device_id = g_utf16_to_utf8 ((gunichar2 *) wid,
      -1, nullptr, nullptr, nullptr);
  CoTaskMemFree (wid);

  PROPVARIANT var;
  PropVariantInit (&var);
  hr = rst_prop->GetValue (PKEY_Device_FriendlyName, &var);
  if (gst_wasapi2_result (hr)) {
    *actual_device_name = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
        -1, nullptr, nullptr, nullptr);
    PropVariantClear (&var);
  }

  *device = rst_device.Detach ();
  *prop = rst_prop.Detach ();
  return;
}

static gboolean
gst_wasapi2_enumerator_execute (GstWasapi2Enumerator * self,
    IMMDeviceCollection * collection, gboolean ignore_error)
{
  auto priv = self->priv;

  GST_DEBUG_OBJECT (self, "Start enumerate");

  UINT count = 0;
  auto hr = collection->GetCount (&count);
  if (!gst_wasapi2_result (hr) || count == 0)
    return TRUE;

  ComPtr < IAudioClient > default_capture_client;
  ComPtr < IAudioClient > default_render_client;
  if (priv->capture_activator)
    priv->capture_activator->GetClient (&default_capture_client, 10000);
  if (priv->render_activator)
    priv->render_activator->GetClient (&default_render_client, 10000);

  ComPtr < IMMDevice > default_capture_device;
  ComPtr < IPropertyStore > default_capture_prop;
  gchar *default_capture_device_id = nullptr;
  gchar *default_capture_device_name = nullptr;

  ComPtr < IMMDevice > default_render_device;
  ComPtr < IPropertyStore > default_render_prop;
  gchar *default_render_device_id = nullptr;
  gchar *default_render_device_name = nullptr;

  get_default_device (self, eCapture, &default_capture_device,
      &default_capture_prop,
      &default_capture_device_id, &default_capture_device_name);
  get_default_device (self, eRender, &default_render_device,
      &default_render_prop,
      &default_render_device_id, &default_render_device_name);

  if (priv->capture_activator && !default_capture_client &&
      default_capture_device) {
    default_capture_device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
        nullptr, &default_capture_client);
  }

  if (priv->render_activator && !default_render_client && default_render_device) {
    default_render_device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
        nullptr, &default_render_client);
  }

  if (default_capture_client) {
    GstWasapi2DeviceProps props;
    props.form_factor = UnknownFormFactor;
    props.enumerator_name = "UNKNOWN";

    if (default_capture_prop)
      gst_wasapi2_enumerator_probe_props (default_capture_prop.Get (), &props);

    g_ptr_array_set_size (priv->endpoint_formats, 0);
    gst_wasapi2_get_shared_mode_formats (default_capture_client.Get (),
        priv->endpoint_formats);
    auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
    g_ptr_array_set_size (priv->endpoint_formats, 0);

    if (caps) {
      auto entry = gst_wasapi2_enumerator_build_entry (self,
          caps, eCapture, TRUE,
          g_strdup (gst_wasapi2_get_default_device_id (eCapture)),
          g_strdup ("Default Audio Capture Device"),
          g_strdup (default_capture_device_id),
          g_strdup (default_capture_device_name), &props);

      if (entry)
        g_ptr_array_add (priv->device_list, entry);
    }
  }

  if (default_render_client) {
    GstWasapi2DeviceProps props;
    props.form_factor = UnknownFormFactor;
    props.enumerator_name = "UNKNOWN";

    if (default_render_prop)
      gst_wasapi2_enumerator_probe_props (default_render_prop.Get (), &props);

    g_ptr_array_set_size (priv->endpoint_formats, 0);
    gst_wasapi2_get_shared_mode_formats (default_render_client.Get (),
        priv->endpoint_formats);
    auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
    g_ptr_array_set_size (priv->endpoint_formats, 0);

    if (caps) {
      auto entry = gst_wasapi2_enumerator_build_entry (self,
          caps, eRender, TRUE,
          g_strdup (gst_wasapi2_get_default_device_id (eRender)),
          g_strdup ("Default Audio Render Device"),
          g_strdup (default_render_device_id),
          g_strdup (default_render_device_name), &props);

      if (entry)
        g_ptr_array_add (priv->device_list, entry);
    }
  }

  for (UINT i = 0; i < count; i++) {
    ComPtr < IMMDevice > device;
    ComPtr < IMMEndpoint > endpoint;
    EDataFlow flow;

    GstWasapi2DeviceProps props;
    props.form_factor = UnknownFormFactor;
    props.enumerator_name = "UNKNOWN";

    hr = collection->Item (i, &device);
    if (!gst_wasapi2_result (hr))
      continue;

    hr = device.As (&endpoint);
    if (!gst_wasapi2_result (hr))
      continue;

    hr = endpoint->GetDataFlow (&flow);
    if (!gst_wasapi2_result (hr))
      continue;

    ComPtr < IPropertyStore > prop;
    hr = device->OpenPropertyStore (STGM_READ, &prop);
    if (!gst_wasapi2_result (hr))
      continue;

    PROPVARIANT var;
    PropVariantInit (&var);
    hr = prop->GetValue (PKEY_Device_FriendlyName, &var);
    if (!gst_wasapi2_result (hr))
      continue;

    auto desc = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
        -1, nullptr, nullptr, nullptr);
    PropVariantClear (&var);

    LPWSTR wid = nullptr;
    hr = device->GetId (&wid);
    if (!gst_wasapi2_result (hr)) {
      g_free (desc);
      continue;
    }

    auto device_id = g_utf16_to_utf8 ((gunichar2 *) wid,
        -1, nullptr, nullptr, nullptr);
    CoTaskMemFree (wid);

    ComPtr < IAudioClient > client;
    hr = device->Activate (__uuidof (IAudioClient), CLSCTX_ALL, nullptr,
        &client);

    if (!gst_wasapi2_result (hr)) {
      /* Requested active devices via DEVICE_STATE_ACTIVE but activate fail here.
       * That means devices were changed while we were enumerating.
       * Need retry here */
      GST_DEBUG_OBJECT (self, "Couldn't activate device %s (%s)",
          device_id, desc);
      g_free (device_id);
      g_free (desc);

      if (!ignore_error && hr == AUDCLNT_E_DEVICE_INVALIDATED)
        return FALSE;
    }

    gst_wasapi2_enumerator_probe_props (prop.Get (), &props);

    g_ptr_array_set_size (priv->endpoint_formats, 0);
    gst_wasapi2_get_shared_mode_formats (client.Get (), priv->endpoint_formats);
    auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
    g_ptr_array_set_size (priv->endpoint_formats, 0);

    if (caps) {
      auto entry = gst_wasapi2_enumerator_build_entry (self, caps, flow,
          FALSE, device_id, desc, nullptr, nullptr, &props);
      if (entry) {
        g_ptr_array_set_size (priv->endpoint_formats, 0);
        gst_wasapi2_get_exclusive_mode_formats (client.Get (),
            prop.Get (), priv->endpoint_formats);
        auto exclusive_caps =
            gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
        g_ptr_array_set_size (priv->endpoint_formats, 0);
        entry->exclusive_caps = exclusive_caps;

        REFERENCE_TIME default_period = 0;
        REFERENCE_TIME min_period = 0;
        WAVEFORMATEX *mix_format = nullptr;

        hr = client->GetDevicePeriod (&default_period, &min_period);
        if (SUCCEEDED (hr)) {
          entry->default_device_period_us = default_period / 10;
          entry->min_device_period_us = min_period / 10;
        }

        client->GetMixFormat (&mix_format);
        if (mix_format) {
          ComPtr < IAudioClient3 > client3;
          hr = client.As (&client3);
          if (SUCCEEDED (hr)) {
            UINT32 default_period_frame = 0;
            UINT32 fundamental_period_frame = 0;
            UINT32 min_period_frame = 0;
            UINT32 max_period_frame = 0;

            hr = client3->GetSharedModeEnginePeriod (mix_format,
                &default_period_frame, &fundamental_period_frame,
                &min_period_frame, &max_period_frame);
            if (SUCCEEDED (hr)) {
              entry->shared_mode_engine_default_period_us =
                  (default_period_frame * 1000000ULL) /
                  mix_format->nSamplesPerSec;
              entry->shared_mode_engine_fundamental_period_us =
                  (fundamental_period_frame * 1000000ULL) /
                  mix_format->nSamplesPerSec;
              entry->shared_mode_engine_min_period_us =
                  (min_period_frame * 1000000ULL) / mix_format->nSamplesPerSec;
              entry->shared_mode_engine_max_period_us =
                  (max_period_frame * 1000000ULL) / mix_format->nSamplesPerSec;
            }
          }

          CoTaskMemFree (mix_format);
        }

        g_ptr_array_add (priv->device_list, entry);
      }
    }
  }

  g_free (default_capture_device_id);
  g_free (default_capture_device_name);
  g_free (default_render_device_id);
  g_free (default_render_device_name);

  return TRUE;
}

static gboolean
gst_wasapi2_enumerator_enumerate_internal (EnumerateData * data)
{
  auto self = data->self;
  auto priv = self->priv;
  /* Upto 3 times retry */
  const guint num_retry = 5;

  for (guint i = 0; i < num_retry; i++) {
    ComPtr < IMMDeviceCollection > collection;
    gboolean is_last = FALSE;

    if (i + 1 == num_retry)
      is_last = TRUE;

    g_ptr_array_set_size (priv->device_list, 0);

    auto hr = priv->handle->EnumAudioEndpoints (eAll, DEVICE_STATE_ACTIVE,
        &collection);
    if (!gst_wasapi2_result (hr)) {
      SetEvent (data->event);
      return G_SOURCE_REMOVE;
    }

    if (gst_wasapi2_enumerator_execute (self, collection.Get (), is_last))
      break;

    if (!is_last) {
      GST_DEBUG_OBJECT (self, "Sleep for retrying");
      Sleep (50);
    }
  }

  while (priv->device_list->len > 0) {
    g_ptr_array_add (data->device_list,
        g_ptr_array_steal_index (priv->device_list, 0));
  }

  SetEvent (data->event);
  return G_SOURCE_REMOVE;
}

void
gst_wasapi2_enumerator_enumerate_devices (GstWasapi2Enumerator * object,
    GPtrArray * device_list)
{
  EnumerateData data;

  data.self = object;
  data.device_list = device_list;

  g_main_context_invoke (object->context,
      (GSourceFunc) gst_wasapi2_enumerator_enumerate_internal, &data);

  WaitForSingleObject (data.event, INFINITE);
}

const gchar *
gst_wasapi2_form_factor_to_string (EndpointFormFactor form_factor)
{
  switch (form_factor) {
    case RemoteNetworkDevice:
      return "RemoteNetworkDevice";
    case Speakers:
      return "Speakers";
    case LineLevel:
      return "LineLevel";
    case Microphone:
      return "Microphone";
    case Headset:
      return "Headset";
    case Handset:
      return "Handset";
    case UnknownDigitalPassthrough:
      return "UnknownDigitalPassthrough";
    case SPDIF:
      return "SPDIF";
    case DigitalAudioDisplayDevice:
      return "DigitalAudioDisplayDevice";
    case UnknownFormFactor:
    default:
      return "UnknownFormFactor";
  }
}
