/*
 * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>
 * Copyright (C) 2013 Collabora Ltd.
 *   Author: Sebastian Dröge <sebastian.droege@collabora.co.uk>
 * Copyright (C) 2018 Centricular Ltd.
 *   Author: Nirbheek Chauhan <nirbheek@centricular.com>
 * Copyright (C) 2020 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 "AsyncOperations.h"
#include "gstwasapi2client.h"
#include "gstwasapi2util.h"
#include <initguid.h>
#include <windows.foundation.h>
#include <windows.ui.core.h>
#include <wrl.h>
#include <wrl/wrappers/corewrappers.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <string.h>
#include <string>
#include <locale>
#include <codecvt>
#include <atomic>

/* *INDENT-OFF* */
using namespace ABI::Windows::ApplicationModel::Core;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::UI::Core;
using namespace ABI::Windows::Media::Devices;
using namespace ABI::Windows::Devices::Enumeration;

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

/* Copy of audioclientactivationparams.h since those types are defined only for
 * NTDDI_VERSION >= NTDDI_WIN10_FE */
#define GST_VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK L"VAD\\Process_Loopback"
typedef enum
{
  GST_PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE = 0,
  GST_PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE = 1
} GST_PROCESS_LOOPBACK_MODE;

typedef struct
{
  DWORD TargetProcessId;
  GST_PROCESS_LOOPBACK_MODE ProcessLoopbackMode;
} GST_AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS;

typedef enum
{
  GST_AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT = 0,
  GST_AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK = 1
} GST_AUDIOCLIENT_ACTIVATION_TYPE;

typedef struct
{
  GST_AUDIOCLIENT_ACTIVATION_TYPE ActivationType;
  union
  {
    GST_AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS ProcessLoopbackParams;
  } DUMMYUNIONNAME;
} GST_AUDIOCLIENT_ACTIVATION_PARAMS;
/* End of audioclientactivationparams.h */

G_BEGIN_DECLS

GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_client_debug);
#define GST_CAT_DEFAULT gst_wasapi2_client_debug

G_END_DECLS
/* *INDENT-ON* */

static void
gst_wasapi2_client_on_device_activated (GstWasapi2Client * client,
    IAudioClient * audio_client);

static void
gst_wasapi2_client_on_endpoint_volume_activated (GstWasapi2Client * client,
    IAudioEndpointVolume * audio_endpoint_volume);

static void
gst_wasapi2_client_set_endpoint_muted (GstWasapi2Client * client,
    gboolean muted);

/* *INDENT-OFF* */
class GstWasapiDeviceActivator
    : public RuntimeClass<RuntimeClassFlags<ClassicCom>, FtmBase,
        IActivateAudioInterfaceCompletionHandler>
{
public:
  typedef enum {
    WASAPI_IFACE_AUDIO_CLIENT,
    WASAPI_IFACE_AUDIO_ENDPOINT_VOLUME,
  } WasapiInterface;

  GstWasapiDeviceActivator ()
  {
    g_weak_ref_init (&listener_, nullptr);
    interface_to_activate_ = WASAPI_IFACE_AUDIO_CLIENT;
  }

  ~GstWasapiDeviceActivator ()
  {
    g_weak_ref_set (&listener_, nullptr);
  }

  HRESULT
  RuntimeClassInitialize (GstWasapi2Client * listener,
      gpointer dispatcher,
      WasapiInterface interface_to_activate)
  {
    if (!listener)
      return E_INVALIDARG;

    g_weak_ref_set (&listener_, listener);

    if (dispatcher) {
      ComPtr<IInspectable> inspectable =
        reinterpret_cast<IInspectable*> (dispatcher);
      HRESULT hr;

      hr = inspectable.As (&dispatcher_);
      if (gst_wasapi2_result (hr))
        GST_INFO("Main UI dispatcher is available");
    }

    interface_to_activate_ = interface_to_activate;

    return S_OK;
  }

  STDMETHOD(ActivateCompleted)
  (IActivateAudioInterfaceAsyncOperation *async_op)
  {
    ComPtr<IAudioClient> audio_client;
    ComPtr<IAudioEndpointVolume> audio_endpoint_volume;
    HRESULT hr = S_OK;
    HRESULT hr_async_op = S_OK;
    ComPtr<IUnknown> audio_interface;
    GstWasapi2Client *client;

    client = (GstWasapi2Client *) g_weak_ref_get (&listener_);

    if (!client) {
      GST_WARNING ("No listener was configured");
      return S_OK;
    }

    GST_INFO_OBJECT (client, "AsyncOperation done");

    hr = async_op->GetActivateResult(&hr_async_op, &audio_interface);

    if (!gst_wasapi2_result (hr)) {
      GST_WARNING_OBJECT (client, "Failed to get activate result, hr: 0x%x", hr);
      goto done;
    }

    if (!gst_wasapi2_result (hr_async_op)) {
      GST_WARNING_OBJECT (client, "Failed to activate device");
      goto done;
    }

    switch (interface_to_activate_) {
      case WASAPI_IFACE_AUDIO_CLIENT:
        hr = audio_interface.As (&audio_client);
        if (!gst_wasapi2_result (hr)) {
          GST_ERROR_OBJECT (client, "Failed to get IAudioClient3 interface");
          goto done;
        }
        break;
      case WASAPI_IFACE_AUDIO_ENDPOINT_VOLUME:
        hr = audio_interface.As (&audio_endpoint_volume);
        if (!gst_wasapi2_result (hr)) {
          GST_ERROR_OBJECT (client, "Failed to get IAudioEndpointVolume interface");
          goto done;
        }
        break;
    }

  done:
    /* Should call this method anyway, listener will wait this event */
    switch (interface_to_activate_) {
      case WASAPI_IFACE_AUDIO_CLIENT:
        gst_wasapi2_client_on_device_activated (client, audio_client.Get());
        break;
      case WASAPI_IFACE_AUDIO_ENDPOINT_VOLUME:
        gst_wasapi2_client_on_endpoint_volume_activated (client, audio_endpoint_volume.Get());
        break;
    }

    gst_object_unref (client);
    /* return S_OK anyway, but listener can know it's succeeded or not
     * by passed IAudioClient handle via gst_wasapi2_client_on_device_activated
     */

    return S_OK;
  }

  HRESULT
  ActivateDeviceAsync(const std::wstring &device_id,
      GST_AUDIOCLIENT_ACTIVATION_PARAMS * params)
  {
    ComPtr<IAsyncAction> async_action;
    bool run_async = false;
    HRESULT hr;

    auto work_item = Callback<Implements<RuntimeClassFlags<ClassicCom>,
        IDispatchedHandler, FtmBase>>([this, device_id, params]{
      ComPtr<IActivateAudioInterfaceAsyncOperation> async_op;
      HRESULT async_hr = S_OK;
      PROPVARIANT activate_params = {};
      IID iid = {};

      switch (interface_to_activate_) {
        case WASAPI_IFACE_AUDIO_CLIENT:
          iid = __uuidof (IAudioClient);
          break;
        case WASAPI_IFACE_AUDIO_ENDPOINT_VOLUME:
          iid = __uuidof (IAudioEndpointVolume);
          break;
      }

      if (params) {
        activate_params.vt = VT_BLOB;
        activate_params.blob.cbSize = sizeof(GST_AUDIOCLIENT_ACTIVATION_PARAMS);
        activate_params.blob.pBlobData = (BYTE *) params;

        async_hr = ActivateAudioInterfaceAsync (device_id.c_str (),
              iid, &activate_params, this, &async_op);
      } else {
        async_hr = ActivateAudioInterfaceAsync (device_id.c_str (),
              iid, nullptr, this, &async_op);
      }

      /* for debugging */
      gst_wasapi2_result (async_hr);

      return async_hr;
    });

    if (dispatcher_) {
      boolean can_now;
      hr = dispatcher_->get_HasThreadAccess (&can_now);

      if (!gst_wasapi2_result (hr))
        return hr;

      if (!can_now)
        run_async = true;
    }

    if (run_async && dispatcher_) {
      hr = dispatcher_->RunAsync (CoreDispatcherPriority_Normal,
          work_item.Get (), &async_action);
    } else {
      hr = work_item->Invoke ();
    }

    return hr;
  }

private:
  GWeakRef listener_;
  ComPtr<ICoreDispatcher> dispatcher_;
  WasapiInterface interface_to_activate_;
};

class GstWasapiEndpointVolumeCallback
    : public RuntimeClass<RuntimeClassFlags<ClassicCom>, FtmBase,
        IAudioEndpointVolumeCallback>
{
public:
  GstWasapiEndpointVolumeCallback ()
  {
    g_weak_ref_init (&client_, nullptr);
  }

  ~GstWasapiEndpointVolumeCallback ()
  {
    g_weak_ref_set (&client_, nullptr);
  }

  HRESULT
  RuntimeClassInitialize (GstWasapi2Client * client)
  {
    if (!client)
      return E_INVALIDARG;
    g_weak_ref_set (&client_, client);
    return S_OK;
  }

  STDMETHOD(OnNotify)
  (AUDIO_VOLUME_NOTIFICATION_DATA * notify)
  {
    GstWasapi2Client *client = (GstWasapi2Client *) g_weak_ref_get (&client_);
    if (client) {
      gst_wasapi2_client_set_endpoint_muted (client, notify->bMuted);
      gst_object_unref (client);
    }
    return S_OK;
  }

private:
  GWeakRef client_;
};
/* *INDENT-ON* */

typedef enum
{
  GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND = -2,
  GST_WASAPI2_CLIENT_ACTIVATE_FAILED = -1,
  GST_WASAPI2_CLIENT_ACTIVATE_INIT = 0,
  GST_WASAPI2_CLIENT_ACTIVATE_WAIT,
  GST_WASAPI2_CLIENT_ACTIVATE_DONE,
} GstWasapi2ClientActivateState;

enum
{
  PROP_0,
  PROP_DEVICE,
  PROP_DEVICE_NAME,
  PROP_DEVICE_INDEX,
  PROP_DEVICE_CLASS,
  PROP_DISPATCHER,
  PROP_CAN_AUTO_ROUTING,
  PROP_LOOPBACK_TARGET_PID,
};

#define DEFAULT_DEVICE_INDEX  -1
#define DEFAULT_DEVICE_CLASS  GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE

struct GstWasapi2ClientPrivate
{
  std::atomic < bool >is_endpoint_muted;
};

struct _GstWasapi2Client
{
  GstObject parent;

  GstWasapi2ClientPrivate *priv;

  GstWasapi2ClientDeviceClass device_class;
  gchar *device_id;
  gchar *device_name;
  gint device_index;
  gpointer dispatcher;
  gboolean can_auto_routing;
  guint target_pid;

  IAudioClient *audio_client;

  GMutex endpoint_volume_lock;
  IAudioEndpointVolume *audio_endpoint_volume;
  GstWasapiEndpointVolumeCallback *endpoint_volume_callback;

  GstCaps *supported_caps;

  GThread *thread;
  GMutex lock;
  GCond cond;
  GMainContext *context;
  GMainLoop *loop;

  /* To wait ActivateCompleted event */
  GMutex init_lock;
  GCond init_cond;
  GstWasapi2ClientActivateState activate_state;
};

GType
gst_wasapi2_client_device_class_get_type (void)
{
  static GType class_type = 0;
  static const GEnumValue types[] = {
    {GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE, "Capture", "capture"},
    {GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, "Render", "render"},
    {GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE, "Loopback-Capture",
        "loopback-capture"},
    {GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE,
          "Include-Process-Loopback-Capture",
        "include-process-loopback-capture"},
    {GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE,
          "Exclude-Process-Loopback-Capture",
        "exclude-process-loopback-capture"},
    {0, nullptr, nullptr}
  };

  if (g_once_init_enter (&class_type)) {
    GType gtype = g_enum_register_static ("GstWasapi2ClientDeviceClass", types);
    g_once_init_leave (&class_type, gtype);
  }

  return class_type;
}

static void gst_wasapi2_client_constructed (GObject * object);
static void gst_wasapi2_client_finalize (GObject * object);
static void gst_wasapi2_client_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_wasapi2_client_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);

static gpointer gst_wasapi2_client_thread_func (GstWasapi2Client * self);
static gboolean
gst_wasapi2_client_main_loop_running_cb (GstWasapi2Client * self);

#define gst_wasapi2_client_parent_class parent_class
G_DEFINE_TYPE (GstWasapi2Client, gst_wasapi2_client, GST_TYPE_OBJECT);

static void
gst_wasapi2_client_class_init (GstWasapi2ClientClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GParamFlags param_flags =
      (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
      G_PARAM_STATIC_STRINGS);

  gobject_class->constructed = gst_wasapi2_client_constructed;
  gobject_class->finalize = gst_wasapi2_client_finalize;
  gobject_class->get_property = gst_wasapi2_client_get_property;
  gobject_class->set_property = gst_wasapi2_client_set_property;

  g_object_class_install_property (gobject_class, PROP_DEVICE,
      g_param_spec_string ("device", "Device",
          "Audio device ID as provided by "
          "Windows.Devices.Enumeration.DeviceInformation.Id",
          nullptr, param_flags));
  g_object_class_install_property (gobject_class, PROP_DEVICE_NAME,
      g_param_spec_string ("device-name", "Device Name",
          "The human-readable device name", nullptr, param_flags));
  g_object_class_install_property (gobject_class, PROP_DEVICE_INDEX,
      g_param_spec_int ("device-index", "Device Index",
          "The zero-based device index", -1, G_MAXINT, DEFAULT_DEVICE_INDEX,
          param_flags));
  g_object_class_install_property (gobject_class, PROP_DEVICE_CLASS,
      g_param_spec_enum ("device-class", "Device Class",
          "Device class", GST_TYPE_WASAPI2_CLIENT_DEVICE_CLASS,
          DEFAULT_DEVICE_CLASS, param_flags));
  g_object_class_install_property (gobject_class, PROP_DISPATCHER,
      g_param_spec_pointer ("dispatcher", "Dispatcher",
          "ICoreDispatcher COM object to use", param_flags));
  g_object_class_install_property (gobject_class, PROP_CAN_AUTO_ROUTING,
      g_param_spec_boolean ("auto-routing", "Auto Routing",
          "Whether client can support automatic stream routing", FALSE,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (gobject_class, PROP_LOOPBACK_TARGET_PID,
      g_param_spec_uint ("loopback-target-pid", "Loopback Target PID",
          "Target process id to record", 0, G_MAXUINT32, 0, param_flags));
}

static void
gst_wasapi2_client_init (GstWasapi2Client * self)
{
  self->device_index = DEFAULT_DEVICE_INDEX;
  self->device_class = DEFAULT_DEVICE_CLASS;
  self->can_auto_routing = FALSE;

  g_mutex_init (&self->lock);
  g_cond_init (&self->cond);

  g_mutex_init (&self->init_lock);
  g_cond_init (&self->init_cond);
  self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_INIT;

  g_mutex_init (&self->endpoint_volume_lock);

  self->context = g_main_context_new ();
  self->loop = g_main_loop_new (self->context, FALSE);

  self->priv = new GstWasapi2ClientPrivate ();
  self->priv->is_endpoint_muted.store (false, std::memory_order_release);
}

static void
gst_wasapi2_client_constructed (GObject * object)
{
  GstWasapi2Client *self = GST_WASAPI2_CLIENT (object);

  /* Create a new thread to ensure that COM thread can be MTA thread.
   * We cannot ensure whether CoInitializeEx() was called outside of here for
   * this thread or not. If it was called with non-COINIT_MULTITHREADED option,
   * we cannot update it */
  g_mutex_lock (&self->lock);
  self->thread = g_thread_new ("GstWasapi2ClientWinRT",
      (GThreadFunc) gst_wasapi2_client_thread_func, self);
  while (!self->loop || !g_main_loop_is_running (self->loop))
    g_cond_wait (&self->cond, &self->lock);
  g_mutex_unlock (&self->lock);

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

static void
gst_wasapi2_client_finalize (GObject * object)
{
  GstWasapi2Client *self = GST_WASAPI2_CLIENT (object);

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

    self->thread = nullptr;
    self->context = nullptr;
    self->loop = nullptr;
  }

  gst_clear_caps (&self->supported_caps);

  g_free (self->device_id);
  g_free (self->device_name);

  g_mutex_clear (&self->lock);
  g_cond_clear (&self->cond);

  g_mutex_clear (&self->init_lock);
  g_cond_clear (&self->init_cond);

  g_mutex_clear (&self->endpoint_volume_lock);

  delete self->priv;

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

static void
gst_wasapi2_client_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstWasapi2Client *self = GST_WASAPI2_CLIENT (object);

  switch (prop_id) {
    case PROP_DEVICE:
      g_value_set_string (value, self->device_id);
      break;
    case PROP_DEVICE_NAME:
      g_value_set_string (value, self->device_name);
      break;
    case PROP_DEVICE_INDEX:
      g_value_set_int (value, self->device_index);
      break;
    case PROP_DEVICE_CLASS:
      g_value_set_enum (value, self->device_class);
      break;
    case PROP_DISPATCHER:
      g_value_set_pointer (value, self->dispatcher);
      break;
    case PROP_CAN_AUTO_ROUTING:
      g_value_set_boolean (value, self->can_auto_routing);
      break;
    case PROP_LOOPBACK_TARGET_PID:
      g_value_set_uint (value, self->target_pid);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_wasapi2_client_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstWasapi2Client *self = GST_WASAPI2_CLIENT (object);

  switch (prop_id) {
    case PROP_DEVICE:
      g_free (self->device_id);
      self->device_id = g_value_dup_string (value);
      break;
    case PROP_DEVICE_NAME:
      g_free (self->device_name);
      self->device_name = g_value_dup_string (value);
      break;
    case PROP_DEVICE_INDEX:
      self->device_index = g_value_get_int (value);
      break;
    case PROP_DEVICE_CLASS:
      self->device_class =
          (GstWasapi2ClientDeviceClass) g_value_get_enum (value);
      break;
    case PROP_DISPATCHER:
      self->dispatcher = g_value_get_pointer (value);
      break;
    case PROP_LOOPBACK_TARGET_PID:
      self->target_pid = g_value_get_uint (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_wasapi2_client_main_loop_running_cb (GstWasapi2Client * self)
{
  GST_DEBUG_OBJECT (self, "Main loop running now");

  g_mutex_lock (&self->lock);
  g_cond_signal (&self->cond);
  g_mutex_unlock (&self->lock);

  return G_SOURCE_REMOVE;
}

static void
gst_wasapi2_client_on_device_activated (GstWasapi2Client * self,
    IAudioClient * audio_client)
{
  GST_INFO_OBJECT (self, "Device activated");

  g_mutex_lock (&self->init_lock);
  if (audio_client) {
    audio_client->AddRef ();
    self->audio_client = audio_client;
    self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_DONE;
  } else {
    GST_WARNING_OBJECT (self, "IAudioClient is unavailable");
    self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_FAILED;
  }
  g_cond_broadcast (&self->init_cond);
  g_mutex_unlock (&self->init_lock);
}

static void
gst_wasapi2_client_on_endpoint_volume_activated (GstWasapi2Client * self,
    IAudioEndpointVolume * audio_endpoint_volume)
{
  GST_INFO_OBJECT (self, "Audio Endpoint Volume activated");

  if (audio_endpoint_volume) {
    HRESULT hr;
    ComPtr < GstWasapiEndpointVolumeCallback > callback;

    g_mutex_lock (&self->endpoint_volume_lock);
    audio_endpoint_volume->AddRef ();
    self->audio_endpoint_volume = audio_endpoint_volume;

    hr = MakeAndInitialize < GstWasapiEndpointVolumeCallback > (&callback,
        self);
    if (!gst_wasapi2_result (hr)) {
      GST_WARNING_OBJECT (self,
          "Could not create endpoint volume callback object");
    } else {
      hr = audio_endpoint_volume->RegisterControlChangeNotify (callback.Get ());
      if (!gst_wasapi2_result (hr)) {
        GST_WARNING_OBJECT (self,
            "Failed to register endpoint volume callback");
      } else {
        BOOL initially_muted = FALSE;

        self->endpoint_volume_callback = callback.Detach ();

        hr = audio_endpoint_volume->GetMute (&initially_muted);
        if (gst_wasapi2_result (hr)) {
          gst_wasapi2_client_set_endpoint_muted (self, initially_muted);
        }
      }
    }
    g_mutex_unlock (&self->endpoint_volume_lock);
  } else {
    GST_WARNING_OBJECT (self, "IAudioEndpointVolume is unavailable");
  }
}

static void
gst_wasapi2_client_set_endpoint_muted (GstWasapi2Client * self, gboolean muted)
{
  GST_DEBUG_OBJECT (self, "Audio Endpoint Volume: muted=%d", muted);

  self->priv->is_endpoint_muted.store (muted, std::memory_order_release);
}

/* *INDENT-OFF* */
static std::string
convert_wstring_to_string (const std::wstring &wstr)
{
  std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;

  return converter.to_bytes (wstr.c_str());
}

static std::string
convert_hstring_to_string (HString * hstr)
{
  const wchar_t *raw_hstr;

  if (!hstr)
    return std::string();

  raw_hstr = hstr->GetRawBuffer (nullptr);
  if (!raw_hstr)
    return std::string();

  return convert_wstring_to_string (std::wstring (raw_hstr));
}

static std::wstring
gst_wasapi2_client_get_default_device_id (GstWasapi2Client * self)
{
  HRESULT hr;
  PWSTR default_device_id_wstr = nullptr;

  if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE)
    hr = StringFromIID (DEVINTERFACE_AUDIO_CAPTURE, &default_device_id_wstr);
  else
    hr = StringFromIID (DEVINTERFACE_AUDIO_RENDER, &default_device_id_wstr);

  if (!gst_wasapi2_result (hr))
    return std::wstring();

  std::wstring ret = std::wstring (default_device_id_wstr);
  CoTaskMemFree (default_device_id_wstr);

  return ret;
}
/* *INDENT-ON* */

static void
gst_wasapi2_client_activate_async (GstWasapi2Client * self,
    GstWasapiDeviceActivator * activator,
    GstWasapiDeviceActivator * endpoint_volume_activator)
{
  /* *INDENT-OFF* */
  ComPtr<IDeviceInformationStatics> device_info_static;
  ComPtr<IAsyncOperation<DeviceInformationCollection*>> async_op;
  ComPtr<IVectorView<DeviceInformation*>> device_list;
  HStringReference hstr_device_info =
      HStringReference(RuntimeClass_Windows_Devices_Enumeration_DeviceInformation);
  /* *INDENT-ON* */
  HRESULT hr;
  DeviceClass device_class;
  unsigned int count = 0;
  gint device_index = 0;
  std::wstring default_device_id_wstring;
  std::string default_device_id;
  std::wstring target_device_id_wstring;
  std::string target_device_id;
  std::string target_device_name;
  gboolean use_default_device = FALSE;
  GST_AUDIOCLIENT_ACTIVATION_PARAMS activation_params;
  gboolean process_loopback = FALSE;

  memset (&activation_params, 0, sizeof (GST_AUDIOCLIENT_ACTIVATION_PARAMS));
  activation_params.ActivationType = GST_AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT;

  self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND;

  if (self->device_class ==
      GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE ||
      self->device_class ==
      GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE) {
    if (self->target_pid == 0) {
      GST_ERROR_OBJECT (self, "Process loopback mode without PID");
      return;
    }

    if (!gst_wasapi2_can_process_loopback ()) {
      GST_ERROR_OBJECT (self, "Process loopback is not supported");
      return;
    }

    process_loopback = TRUE;
    activation_params.ActivationType =
        GST_AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK;
    activation_params.ProcessLoopbackParams.TargetProcessId =
        (DWORD) self->target_pid;
    target_device_id_wstring = GST_VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK;
    target_device_id = convert_wstring_to_string (target_device_id_wstring);

    if (self->device_class ==
        GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE) {
      activation_params.ProcessLoopbackParams.ProcessLoopbackMode =
          GST_PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE;
    } else {
      activation_params.ProcessLoopbackParams.ProcessLoopbackMode =
          GST_PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE;
    }

    target_device_name = "Process-loopback";
    goto activate;
  }

  GST_INFO_OBJECT (self,
      "requested device info, device-class: %s, device: %s, device-index: %d",
      self->device_class ==
      GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE ? "capture" : "render",
      GST_STR_NULL (self->device_id), self->device_index);

  if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE) {
    device_class = DeviceClass::DeviceClass_AudioCapture;
  } else {
    device_class = DeviceClass::DeviceClass_AudioRender;
  }

  default_device_id_wstring = gst_wasapi2_client_get_default_device_id (self);
  if (default_device_id_wstring.empty ()) {
    GST_WARNING_OBJECT (self, "Couldn't get default device id");
    return;
  }

  default_device_id = convert_wstring_to_string (default_device_id_wstring);
  GST_DEBUG_OBJECT (self, "Default device id: %s", default_device_id.c_str ());

  /* When
   * 1) default device was requested or
   * 2) no explicitly requested device or
   * 3) requested device string id is null but device index is zero
   * will use default device
   *
   * Note that default device is much preferred
   * See https://docs.microsoft.com/en-us/windows/win32/coreaudio/automatic-stream-routing
   */

  /* DEVINTERFACE_AUDIO_CAPTURE and DEVINTERFACE_AUDIO_RENDER are available
   * as of Windows 10 */
  if (gst_wasapi2_can_automatic_stream_routing ()) {
    if (self->device_id &&
        g_ascii_strcasecmp (self->device_id, default_device_id.c_str ()) == 0) {
      GST_DEBUG_OBJECT (self, "Default device was requested");
      use_default_device = TRUE;
    } else if (self->device_index < 0 && !self->device_id) {
      GST_DEBUG_OBJECT (self,
          "No device was explicitly requested, use default device");
      use_default_device = TRUE;
    } else if (!self->device_id && self->device_index == 0) {
      GST_DEBUG_OBJECT (self, "device-index == zero means default device");
      use_default_device = TRUE;
    }
  }

  if (use_default_device) {
    target_device_id_wstring = default_device_id_wstring;
    target_device_id = default_device_id;
    if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE)
      target_device_name = "Default Audio Capture Device";
    else
      target_device_name = "Default Audio Render Device";
    goto activate;
  }

  hr = GetActivationFactory (hstr_device_info.Get (), &device_info_static);
  if (!gst_wasapi2_result (hr))
    return;

  hr = device_info_static->FindAllAsyncDeviceClass (device_class, &async_op);
  device_info_static.Reset ();
  if (!gst_wasapi2_result (hr))
    return;

  /* *INDENT-OFF* */
  hr = SyncWait<DeviceInformationCollection*>(async_op.Get ());
  /* *INDENT-ON* */
  if (!gst_wasapi2_result (hr))
    return;

  hr = async_op->GetResults (&device_list);
  async_op.Reset ();
  if (!gst_wasapi2_result (hr))
    return;

  hr = device_list->get_Size (&count);
  if (!gst_wasapi2_result (hr))
    return;

  if (count == 0) {
    GST_INFO_OBJECT (self, "No available device");
    return;
  }

  /* device_index 0 will be assigned for default device
   * so the number of available device is count + 1 (for default device) */
  if (self->device_index >= 0 && self->device_index > (gint) count) {
    GST_INFO_OBJECT (self, "Device index %d is unavailable",
        self->device_index);
    return;
  }

  GST_DEBUG_OBJECT (self, "Available device count: %d", count);

  if (gst_wasapi2_can_automatic_stream_routing ()) {
    /* zero is for default device */
    device_index = 1;
  } else {
    device_index = 0;
  }

  for (unsigned int i = 0; i < count; i++) {
    /* *INDENT-OFF* */
    ComPtr<IDeviceInformation> device_info;
    /* *INDENT-ON* */
    HString id;
    HString name;
    boolean b_value;
    std::string cur_device_id;
    std::string cur_device_name;

    hr = device_list->GetAt (i, &device_info);
    if (!gst_wasapi2_result (hr))
      continue;

    hr = device_info->get_IsEnabled (&b_value);
    if (!gst_wasapi2_result (hr))
      continue;

    /* select only enabled device */
    if (!b_value) {
      GST_DEBUG_OBJECT (self, "Device index %d is disabled", i);
      continue;
    }

    /* To ensure device id and device name are available,
     * will query this later again once target device is determined */
    hr = device_info->get_Id (id.GetAddressOf ());
    if (!gst_wasapi2_result (hr))
      continue;

    if (!id.IsValid ()) {
      GST_WARNING_OBJECT (self, "Device index %d has invalid id", i);
      continue;
    }

    hr = device_info->get_Name (name.GetAddressOf ());
    if (!gst_wasapi2_result (hr))
      continue;

    if (!name.IsValid ()) {
      GST_WARNING_OBJECT (self, "Device index %d has invalid name", i);
      continue;
    }

    cur_device_id = convert_hstring_to_string (&id);
    if (cur_device_id.empty ()) {
      GST_WARNING_OBJECT (self, "Device index %d has empty id", i);
      continue;
    }

    cur_device_name = convert_hstring_to_string (&name);
    if (cur_device_name.empty ()) {
      GST_WARNING_OBJECT (self, "Device index %d has empty device name", i);
      continue;
    }

    GST_DEBUG_OBJECT (self, "device [%d] id: %s, name: %s",
        device_index, cur_device_id.c_str (), cur_device_name.c_str ());

    if (self->device_index < 0 && !self->device_id) {
      GST_INFO_OBJECT (self, "Select the first device, device id %s",
          cur_device_id.c_str ());
      target_device_id_wstring = id.GetRawBuffer (nullptr);
      target_device_id = cur_device_id;
      target_device_name = cur_device_name;
      break;
    }

    if (self->device_id &&
        g_ascii_strcasecmp (self->device_id, cur_device_id.c_str ()) == 0) {
      GST_INFO_OBJECT (self,
          "Device index %d has matching device id %s", device_index,
          cur_device_id.c_str ());
      target_device_id_wstring = id.GetRawBuffer (nullptr);
      target_device_id = cur_device_id;
      target_device_name = cur_device_name;
      break;
    }

    if (self->device_index >= 0 && self->device_index == device_index) {
      GST_INFO_OBJECT (self, "Select device index %d, device id %s",
          device_index, cur_device_id.c_str ());
      target_device_id_wstring = id.GetRawBuffer (nullptr);
      target_device_id = cur_device_id;
      target_device_name = cur_device_name;
      break;
    }

    /* count only available devices */
    device_index++;
  }

  if (target_device_id_wstring.empty ()) {
    GST_WARNING_OBJECT (self, "Couldn't find target device");
    return;
  }

activate:
  /* fill device id and name */
  g_free (self->device_id);
  self->device_id = g_strdup (target_device_id.c_str ());

  g_free (self->device_name);
  self->device_name = g_strdup (target_device_name.c_str ());

  self->device_index = device_index;
  /* default device supports automatic stream routing */
  self->can_auto_routing = use_default_device;

  self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_INIT;

  if (process_loopback) {
    hr = activator->ActivateDeviceAsync (target_device_id_wstring,
        &activation_params);
  } else {
    hr = activator->ActivateDeviceAsync (target_device_id_wstring, nullptr);
  }

  if (!gst_wasapi2_result (hr)) {
    GST_WARNING_OBJECT (self, "Failed to activate device");
    self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_FAILED;
    return;
  }

  /* activate the endpoint volume interface */
  if (endpoint_volume_activator) {
    if (use_default_device) {
      GST_INFO_OBJECT (self,
          "Endpoint volume monitoring for the default device is not implemented.");
    } else {
      hr = endpoint_volume_activator->ActivateDeviceAsync
          (target_device_id_wstring, nullptr);
      if (!gst_wasapi2_result (hr)) {
        GST_WARNING_OBJECT (self, "Failed to activate device");
      }
    }
  }

  g_mutex_lock (&self->lock);
  if (self->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_INIT)
    self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_WAIT;
  g_mutex_unlock (&self->lock);
}

static const gchar *
activate_state_to_string (GstWasapi2ClientActivateState state)
{
  switch (state) {
    case GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND:
      return "NOT-FOUND";
    case GST_WASAPI2_CLIENT_ACTIVATE_FAILED:
      return "FAILED";
    case GST_WASAPI2_CLIENT_ACTIVATE_INIT:
      return "INIT";
    case GST_WASAPI2_CLIENT_ACTIVATE_WAIT:
      return "WAIT";
    case GST_WASAPI2_CLIENT_ACTIVATE_DONE:
      return "DONE";
  }

  g_assert_not_reached ();

  return "Undefined";
}

static gpointer
gst_wasapi2_client_thread_func (GstWasapi2Client * self)
{
  RoInitializeWrapper initialize (RO_INIT_MULTITHREADED);
  GSource *source;
  HRESULT hr;
  /* *INDENT-OFF* */
  ComPtr<GstWasapiDeviceActivator> client_activator;
  ComPtr<GstWasapiDeviceActivator> endpoint_volume_activator;

  hr = MakeAndInitialize<GstWasapiDeviceActivator> (&client_activator,
      self, self->dispatcher, GstWasapiDeviceActivator::WASAPI_IFACE_AUDIO_CLIENT);
  /* *INDENT-ON* */

  if (!gst_wasapi2_result (hr)) {
    GST_ERROR_OBJECT (self, "Could not create activator object");
    self->activate_state = GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND;
    goto run_loop;
  }

  /* Initialize audio endpoint volume activator */
  hr = MakeAndInitialize < GstWasapiDeviceActivator >
      (&endpoint_volume_activator, self, self->dispatcher,
      GstWasapiDeviceActivator::WASAPI_IFACE_AUDIO_ENDPOINT_VOLUME);
  if (!gst_wasapi2_result (hr)) {
    GST_WARNING_OBJECT (self,
        "Could not create endpoint volume activator object");
  }

  gst_wasapi2_client_activate_async (self, client_activator.Get (),
      endpoint_volume_activator.Get ());

  if (!self->dispatcher) {
    /* In case that dispatcher is unavailable, wait activation synchroniously */
    GST_DEBUG_OBJECT (self, "Wait device activation");
    gst_wasapi2_client_ensure_activation (self);
    GST_DEBUG_OBJECT (self, "Device activation result %s",
        activate_state_to_string (self->activate_state));
  }

run_loop:
  g_main_context_push_thread_default (self->context);

  source = g_idle_source_new ();
  g_source_set_callback (source,
      (GSourceFunc) gst_wasapi2_client_main_loop_running_cb, self, nullptr);
  g_source_attach (source, self->context);
  g_source_unref (source);

  GST_DEBUG_OBJECT (self, "Starting main loop");
  g_main_loop_run (self->loop);
  GST_DEBUG_OBJECT (self, "Stopped main loop");

  g_main_context_pop_thread_default (self->context);

  /* Wait for pending async op if any */
  if (self->dispatcher)
    gst_wasapi2_client_ensure_activation (self);

  GST_WASAPI2_CLEAR_COM (self->audio_client);

  g_mutex_lock (&self->endpoint_volume_lock);
  if (self->audio_endpoint_volume && self->endpoint_volume_callback) {
    self->audio_endpoint_volume->
        UnregisterControlChangeNotify (self->endpoint_volume_callback);
  }
  GST_WASAPI2_CLEAR_COM (self->endpoint_volume_callback);
  GST_WASAPI2_CLEAR_COM (self->audio_endpoint_volume);
  g_mutex_unlock (&self->endpoint_volume_lock);

  /* Reset explicitly to ensure that it happens before
   * RoInitializeWrapper dtor is called */
  client_activator.Reset ();
  endpoint_volume_activator.Reset ();

  GST_DEBUG_OBJECT (self, "Exit thread function");

  return nullptr;
}

GstCaps *
gst_wasapi2_client_get_caps (GstWasapi2Client * client)
{
  WAVEFORMATEX *mix_format = nullptr;
  static GstStaticCaps static_caps = GST_STATIC_CAPS (GST_WASAPI2_STATIC_CAPS);
  GstCaps *scaps;
  HRESULT hr;

  g_return_val_if_fail (GST_IS_WASAPI2_CLIENT (client), nullptr);

  if (client->supported_caps)
    return gst_caps_ref (client->supported_caps);

  if (!client->audio_client) {
    GST_WARNING_OBJECT (client, "IAudioClient3 wasn't configured");
    return nullptr;
  }

  hr = client->audio_client->GetMixFormat (&mix_format);
  if (!gst_wasapi2_result (hr)) {
    if (gst_wasapi2_device_class_is_process_loopback (client->device_class)) {
      mix_format = gst_wasapi2_get_default_mix_format ();
    } else {
      GST_WARNING_OBJECT (client, "Failed to get mix format");
      return nullptr;
    }
  }

  scaps = gst_static_caps_get (&static_caps);
  gst_wasapi2_util_parse_waveformatex (mix_format,
      scaps, &client->supported_caps, nullptr);
  gst_caps_unref (scaps);

  CoTaskMemFree (mix_format);

  if (!client->supported_caps) {
    GST_ERROR_OBJECT (client, "No caps from subclass");
    return nullptr;
  }

  return gst_caps_ref (client->supported_caps);
}

gboolean
gst_wasapi2_client_ensure_activation (GstWasapi2Client * client)
{
  g_return_val_if_fail (GST_IS_WASAPI2_CLIENT (client), FALSE);

  /* should not happen */
  g_assert (client->activate_state != GST_WASAPI2_CLIENT_ACTIVATE_INIT);

  g_mutex_lock (&client->init_lock);
  while (client->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_WAIT)
    g_cond_wait (&client->init_cond, &client->init_lock);
  g_mutex_unlock (&client->init_lock);

  return client->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_DONE;
}

static HRESULT
find_dispatcher (ICoreDispatcher ** dispatcher)
{
  /* *INDENT-OFF* */
  HStringReference hstr_core_app =
      HStringReference(RuntimeClass_Windows_ApplicationModel_Core_CoreApplication);
  ComPtr<ICoreApplication> core_app;
  ComPtr<ICoreApplicationView> core_app_view;
  ComPtr<ICoreWindow> core_window;
  /* *INDENT-ON* */
  HRESULT hr;

  hr = GetActivationFactory (hstr_core_app.Get (), &core_app);
  if (FAILED (hr))
    return hr;

  hr = core_app->GetCurrentView (&core_app_view);
  if (FAILED (hr))
    return hr;

  hr = core_app_view->get_CoreWindow (&core_window);
  if (FAILED (hr))
    return hr;

  return core_window->get_Dispatcher (dispatcher);
}

GstWasapi2Client *
gst_wasapi2_client_new (GstWasapi2ClientDeviceClass device_class,
    gint device_index, const gchar * device_id, guint32 target_pid,
    gpointer dispatcher)
{
  GstWasapi2Client *self;
  /* *INDENT-OFF* */
  ComPtr<ICoreDispatcher> core_dispatcher;
  /* *INDENT-ON* */
  /* Multiple COM init is allowed */
  RoInitializeWrapper init_wrapper (RO_INIT_MULTITHREADED);

  /* If application didn't pass ICoreDispatcher object,
   * try to get dispatcher object for the current thread */
  if (!dispatcher) {
    HRESULT hr;

    hr = find_dispatcher (&core_dispatcher);
    if (SUCCEEDED (hr)) {
      GST_DEBUG ("UI dispatcher is available");
      dispatcher = core_dispatcher.Get ();
    } else {
      GST_DEBUG ("UI dispatcher is unavailable");
    }
  } else {
    GST_DEBUG ("Use user passed UI dispatcher");
  }

  self = (GstWasapi2Client *) g_object_new (GST_TYPE_WASAPI2_CLIENT,
      "device-class", device_class, "device-index", device_index,
      "device", device_id, "loopback-target-pid", target_pid,
      "dispatcher", dispatcher, nullptr);

  /* Reset explicitly to ensure that it happens before
   * RoInitializeWrapper dtor is called */
  core_dispatcher.Reset ();

  if (self->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_FAILED ||
      self->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND) {
    gst_object_unref (self);
    return nullptr;
  }

  gst_object_ref_sink (self);

  return self;
}

GstWasapi2Result
gst_wasapi2_client_enumerate (GstWasapi2ClientDeviceClass device_class,
    gint device_index, GstWasapi2Client ** client)
{
  GstWasapi2Client *self;
  /* *INDENT-OFF* */
  ComPtr<ICoreDispatcher> core_dispatcher;
  /* *INDENT-ON* */
  /* Multiple COM init is allowed */
  RoInitializeWrapper init_wrapper (RO_INIT_MULTITHREADED);

  *client = nullptr;

  find_dispatcher (&core_dispatcher);

  self = (GstWasapi2Client *) g_object_new (GST_TYPE_WASAPI2_CLIENT,
      "device-class", device_class, "device-index", device_index,
      "dispatcher", core_dispatcher.Get (), nullptr);

  /* Reset explicitly to ensure that it happens before
   * RoInitializeWrapper dtor is called */
  core_dispatcher.Reset ();

  if (self->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_NOT_FOUND) {
    gst_object_unref (self);
    return GST_WASAPI2_DEVICE_NOT_FOUND;
  } else if (self->activate_state == GST_WASAPI2_CLIENT_ACTIVATE_FAILED) {
    gst_object_unref (self);
    return GST_WASAPI2_ACTIVATION_FAILED;
  }

  gst_object_ref_sink (self);

  *client = self;
  return GST_WASAPI2_OK;
}

IAudioClient *
gst_wasapi2_client_get_handle (GstWasapi2Client * client)
{
  g_return_val_if_fail (GST_IS_WASAPI2_CLIENT (client), nullptr);

  return client->audio_client;
}

gboolean
gst_wasapi2_client_is_endpoint_muted (GstWasapi2Client * client)
{
  g_return_val_if_fail (GST_IS_WASAPI2_CLIENT (client), FALSE);

  return client->priv->is_endpoint_muted.load (std::memory_order_acquire);
}
