/*
 * Copyright 2021 Andrew Eikum for CodeWeavers
 * Copyright 2021 Rémi Bernon for CodeWeavers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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
 */
#define COBJMACROS
#include "initguid.h"
#include <stdarg.h>

#include "windef.h"
#include "winbase.h"
#include "winerror.h"
#include "winstring.h"

#include "mmdeviceapi.h"
#include "audioclient.h"
#include "roapi.h"

#define WIDL_using_Windows_Foundation
#define WIDL_using_Windows_Foundation_Collections
#include "windows.foundation.h"
#define WIDL_using_Windows_Media_Devices
#include "windows.media.devices.h"

#include "wine/test.h"

static HRESULT (WINAPI *pActivateAudioInterfaceAsync)(const WCHAR *path,
            REFIID riid, PROPVARIANT *params,
            IActivateAudioInterfaceCompletionHandler *done_handler,
            IActivateAudioInterfaceAsyncOperation **op);

static IMMDeviceEnumerator *g_mmdevenum;
static WCHAR *g_default_capture_id, *g_default_render_id;

static struct {
    LONG ref;
    HANDLE evt;
    CRITICAL_SECTION lock;
    IActivateAudioInterfaceAsyncOperation *op;
    char msg_pfx[128];
    IUnknown *result_iface;
    HRESULT result_hr;
} async_activate_test;

static HRESULT WINAPI async_activate_QueryInterface(
        IActivateAudioInterfaceCompletionHandler *iface,
        REFIID riid,
        void **ppvObject)
{
    if (IsEqualIID(riid, &IID_IUnknown) ||
            IsEqualIID(riid, &IID_IAgileObject) ||
            IsEqualIID(riid, &IID_IActivateAudioInterfaceCompletionHandler))
    {
        *ppvObject = iface;
        IUnknown_AddRef((IUnknown*)*ppvObject);
        return S_OK;
    }

    *ppvObject = NULL;
    return E_NOINTERFACE;
}

static ULONG WINAPI async_activate_AddRef(
        IActivateAudioInterfaceCompletionHandler *iface)
{
    return InterlockedIncrement(&async_activate_test.ref);
}

static ULONG WINAPI async_activate_Release(
        IActivateAudioInterfaceCompletionHandler *iface)
{
    ULONG ref = InterlockedDecrement(&async_activate_test.ref);
    if (ref == 1)
        SetEvent(async_activate_test.evt);
    return ref;
}

static HRESULT WINAPI async_activate_ActivateCompleted(
        IActivateAudioInterfaceCompletionHandler *iface,
        IActivateAudioInterfaceAsyncOperation *op)
{
    HRESULT hr;

    EnterCriticalSection(&async_activate_test.lock);
    ok(op == async_activate_test.op,
            "%s: Got different completion operation\n",
            async_activate_test.msg_pfx);
    LeaveCriticalSection(&async_activate_test.lock);

    hr = IActivateAudioInterfaceAsyncOperation_GetActivateResult(op,
            &async_activate_test.result_hr, &async_activate_test.result_iface);
    ok(hr == S_OK,
            "%s: GetActivateResult failed: %08x\n",
            async_activate_test.msg_pfx, hr);

    return S_OK;
}

static IActivateAudioInterfaceCompletionHandlerVtbl async_activate_vtbl = {
    async_activate_QueryInterface,
    async_activate_AddRef,
    async_activate_Release,
    async_activate_ActivateCompleted,
};

static IActivateAudioInterfaceCompletionHandler async_activate_done = {
    &async_activate_vtbl
};

static void test_MediaDeviceStatics(void)
{
    static const WCHAR *media_device_statics_name = L"Windows.Media.Devices.MediaDevice";

    IMediaDeviceStatics *media_device_statics = NULL;
    IActivationFactory *factory = NULL;
    IInspectable *inspectable = NULL, *tmp_inspectable = NULL;
    IAgileObject *agile_object = NULL, *tmp_agile_object = NULL;
    HANDLE done_evt = CreateEventW(NULL, FALSE, FALSE, NULL);
    IMMDevice *mmdev;
    HSTRING str;
    HRESULT hr;
    DWORD dr;

    hr = WindowsCreateString(media_device_statics_name, wcslen(media_device_statics_name), &str);
    ok(hr == S_OK, "WindowsCreateString failed, hr %#x\n", hr);

    hr = RoGetActivationFactory(str, &IID_IActivationFactory, (void **)&factory);
    ok(hr == S_OK, "RoGetActivationFactory failed, hr %#x\n", hr);
    WindowsDeleteString(str);

    /* interface tests */
    hr = IActivationFactory_QueryInterface(factory, &IID_IInspectable, (void **)&inspectable);
    ok(hr == S_OK, "IActivationFactory_QueryInterface IID_IInspectable failed, hr %#x\n", hr);

    hr = IActivationFactory_QueryInterface(factory, &IID_IAgileObject, (void **)&agile_object);
    ok(hr == S_OK, "IActivationFactory_QueryInterface IID_IAgileObject failed, hr %#x\n", hr);

    hr = IActivationFactory_QueryInterface(factory, &IID_IMediaDeviceStatics, (void **)&media_device_statics);
    ok(hr == S_OK, "IActivationFactory_QueryInterface IID_IMediaDeviceStatics failed, hr %#x\n", hr);

    hr = IMediaDeviceStatics_QueryInterface(media_device_statics, &IID_IInspectable, (void **)&tmp_inspectable);
    ok(hr == S_OK, "IMediaDeviceStatics_QueryInterface IID_IInspectable failed, hr %#x\n", hr);
    ok(tmp_inspectable == inspectable, "IMediaDeviceStatics_QueryInterface IID_IInspectable returned %p, expected %p\n", tmp_inspectable, inspectable);
    IInspectable_Release(tmp_inspectable);

    hr = IMediaDeviceStatics_QueryInterface(media_device_statics, &IID_IAgileObject, (void **)&tmp_agile_object);
    ok(hr == S_OK, "IMediaDeviceStatics_QueryInterface IID_IAgileObject failed, hr %#x\n", hr);
    ok(tmp_agile_object == agile_object, "IMediaDeviceStatics_QueryInterface IID_IAgileObject returned %p, expected %p\n", tmp_agile_object, agile_object);
    IAgileObject_Release(tmp_agile_object);

    IAgileObject_Release(agile_object);
    IInspectable_Release(inspectable);
    IActivationFactory_Release(factory);

    InitializeCriticalSection(&async_activate_test.lock);

    /* test default capture device creation */
    hr = IMediaDeviceStatics_GetDefaultAudioCaptureId(media_device_statics, AudioDeviceRole_Default, &str);
    ok(hr == S_OK, "GetDefaultAudioCaptureId failed: %08x\n", hr);
    ok((!!g_default_capture_id) == (!!str),
            "Presence of default capture device doesn't match expected state\n");

    if (g_default_capture_id)
    {
        ok(wcsstr(WindowsGetStringRawBuffer(str, NULL), g_default_capture_id) != NULL,
                "Expected to find substring of default capture id in %s\n",
                wine_dbgstr_w(WindowsGetStringRawBuffer(str, NULL)));

        /* returned id does not work in GetDevice... */
        hr = IMMDeviceEnumerator_GetDevice(g_mmdevenum, WindowsGetStringRawBuffer(str, NULL), &mmdev);
        ok(hr == E_INVALIDARG, "GetDevice gave wrong error: %08x\n", hr);

        /* ...but does work in ActivateAudioInterfaceAsync */
        async_activate_test.ref = 1;
        async_activate_test.evt = done_evt;
        async_activate_test.op = NULL;
        async_activate_test.result_iface = NULL;
        async_activate_test.result_hr = 0;
        strcpy(async_activate_test.msg_pfx, "capture_activate");

        EnterCriticalSection(&async_activate_test.lock);
        hr = pActivateAudioInterfaceAsync(WindowsGetStringRawBuffer(str, NULL),
                &IID_IAudioClient2, NULL, &async_activate_done, &async_activate_test.op);
        ok(hr == S_OK, "ActivateAudioInterfaceAsync failed: %08x\n", hr);
        LeaveCriticalSection(&async_activate_test.lock);

        IActivateAudioInterfaceAsyncOperation_Release(async_activate_test.op);

        dr = WaitForSingleObject(async_activate_test.evt, 1000); /* wait for all refs other than our own to be released */
        ok(dr == WAIT_OBJECT_0, "Timed out waiting for async activate to complete\n");
        ok(async_activate_test.result_hr == S_OK, "Got unexpected activation result: %08x\n", async_activate_test.result_hr);
        ok(async_activate_test.result_iface != NULL, "Expected to get WASAPI interface, but got NULL\n");
        IUnknown_Release(async_activate_test.result_iface);

        WindowsDeleteString(str);
    }

    /* test default render device creation */
    hr = IMediaDeviceStatics_GetDefaultAudioRenderId(media_device_statics, AudioDeviceRole_Default, &str);
    ok(hr == S_OK, "GetDefaultAudioRenderId failed: %08x\n", hr);
    ok((!!g_default_render_id) == (!!str),
            "Presence of default render device doesn't match expected state\n");

    if (g_default_render_id)
    {
        ok(wcsstr(WindowsGetStringRawBuffer(str, NULL), g_default_render_id) != NULL,
                "Expected to find substring of default render id in %s\n",
                wine_dbgstr_w(WindowsGetStringRawBuffer(str, NULL)));

        /* returned id does not work in GetDevice... */
        hr = IMMDeviceEnumerator_GetDevice(g_mmdevenum, WindowsGetStringRawBuffer(str, NULL), &mmdev);
        ok(hr == E_INVALIDARG, "GetDevice gave wrong error: %08x\n", hr);

        /* ...but does work in ActivateAudioInterfaceAsync */
        async_activate_test.ref = 1;
        async_activate_test.evt = done_evt;
        async_activate_test.op = NULL;
        async_activate_test.result_iface = NULL;
        async_activate_test.result_hr = 0;
        strcpy(async_activate_test.msg_pfx, "render_activate");

        EnterCriticalSection(&async_activate_test.lock);
        hr = pActivateAudioInterfaceAsync(WindowsGetStringRawBuffer(str, NULL),
                &IID_IAudioClient2, NULL, &async_activate_done, &async_activate_test.op);
        ok(hr == S_OK, "ActivateAudioInterfaceAsync failed: %08x\n", hr);
        LeaveCriticalSection(&async_activate_test.lock);

        IActivateAudioInterfaceAsyncOperation_Release(async_activate_test.op);

        dr = WaitForSingleObject(async_activate_test.evt, 1000); /* wait for all refs other than our own to be released */
        ok(dr == WAIT_OBJECT_0, "Timed out waiting for async activate to complete\n");
        ok(async_activate_test.result_hr == S_OK, "Got unexpected activation result: %08x\n", async_activate_test.result_hr);
        ok(async_activate_test.result_iface != NULL, "Expected to get WASAPI interface, but got NULL\n");
        IUnknown_Release(async_activate_test.result_iface);

        WindowsDeleteString(str);
    }

    /* cleanup */
    CloseHandle(done_evt);
    DeleteCriticalSection(&async_activate_test.lock);
    IMediaDeviceStatics_Release(media_device_statics);
}

START_TEST(devices)
{
    HMODULE mmdevapi;
    IMMDevice *mmdev;
    HRESULT hr;

    if (!(mmdevapi = LoadLibraryW(L"mmdevapi.dll")))
    {
        win_skip("Failed to load mmdevapi.dll, skipping tests\n");
        return;
    }

#define LOAD_FUNCPTR(x) \
    if (!(p##x = (void*)GetProcAddress(mmdevapi, #x))) \
    { \
        win_skip("Failed to find %s in mmdevapi.dll, skipping tests.\n", #x); \
        return; \
    }

    LOAD_FUNCPTR(ActivateAudioInterfaceAsync);
#undef LOAD_FUNCPTR

    hr = RoInitialize(RO_INIT_MULTITHREADED);
    ok(hr == S_OK, "RoInitialize failed, hr %#x\n", hr);

    hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL,
            CLSCTX_INPROC_SERVER, &IID_IMMDeviceEnumerator, (void**)&g_mmdevenum);
    ok(hr == S_OK, "Couldn't make MMDeviceEnumerator: %08x\n", hr);

    hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(g_mmdevenum, eCapture, eMultimedia, &mmdev);
    if (hr == S_OK)
    {
        hr = IMMDevice_GetId(mmdev, &g_default_capture_id);
        ok(hr == S_OK, "IMMDevice::GetId(capture) failed: %08x\n", hr);

        IMMDevice_Release(mmdev);
    }
    if (hr != S_OK)
        g_default_capture_id = NULL;

    hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(g_mmdevenum, eRender, eMultimedia, &mmdev);
    if (hr == S_OK)
    {
        hr = IMMDevice_GetId(mmdev, &g_default_render_id);
        ok(hr == S_OK, "IMMDevice::GetId(render) failed: %08x\n", hr);

        IMMDevice_Release(mmdev);
    }
    if (hr != S_OK)
        g_default_render_id = NULL;

    test_MediaDeviceStatics();

    CoTaskMemFree(g_default_capture_id);
    CoTaskMemFree(g_default_render_id);
    IMMDeviceEnumerator_Release(g_mmdevenum);

    RoUninitialize();
}
