/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   JUCE is an open source library subject to commercial or open-source
   licensing.

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   To use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

struct MidiServiceType
{
    struct InputWrapper
    {
        virtual ~InputWrapper() {}

        virtual String getDeviceName() = 0;
        virtual void start() = 0;
        virtual void stop() = 0;
    };

    struct OutputWrapper
    {
        virtual ~OutputWrapper() {}

        virtual String getDeviceName() = 0;
        virtual void sendMessageNow (const MidiMessage&) = 0;
    };

    MidiServiceType() {}
    virtual ~MidiServiceType() {}

    virtual StringArray getDevices (bool) = 0;
    virtual int getDefaultDeviceIndex (bool) = 0;

    virtual InputWrapper* createInputWrapper (MidiInput&, int, MidiInputCallback&) = 0;
    virtual OutputWrapper* createOutputWrapper (int) = 0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiServiceType)
};

//==============================================================================
struct Win32MidiService  : public MidiServiceType,
                           private Timer
{
    Win32MidiService() {}

    StringArray getDevices (bool isInput) override
    {
        return isInput ? Win32InputWrapper::getDevices()
                       : Win32OutputWrapper::getDevices();
    }

    int getDefaultDeviceIndex (bool isInput) override
    {
        return isInput ? Win32InputWrapper::getDefaultDeviceIndex()
                       : Win32OutputWrapper::getDefaultDeviceIndex();
    }

    InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override
    {
        return new Win32InputWrapper (*this, input, index, callback);
    }

    OutputWrapper* createOutputWrapper (int index) override
    {
        return new Win32OutputWrapper (*this, index);
    }

private:
    struct Win32InputWrapper;

    //==============================================================================
    struct MidiInCollector  : public ReferenceCountedObject
    {
        MidiInCollector (Win32MidiService& s, const String& name)  : deviceName (name), midiService (s) {}

        ~MidiInCollector()
        {
            stop();

            if (deviceHandle != 0)
            {
                for (int count = 5; --count >= 0;)
                {
                    if (midiInClose (deviceHandle) == MMSYSERR_NOERROR)
                        break;

                    Sleep (20);
                }
            }
        }

        using Ptr = ReferenceCountedObjectPtr<MidiInCollector>;

        void addClient (Win32InputWrapper* c)
        {
            const ScopedLock sl (clientLock);
            jassert (! clients.contains (c));
            clients.add (c);
        }

        void removeClient (Win32InputWrapper* c)
        {
            const ScopedLock sl (clientLock);
            clients.removeFirstMatchingValue (c);
            startOrStop();
            midiService.asyncCheckForUnusedCollectors();
        }

        void handleMessage (const uint8* bytes, uint32 timeStamp)
        {
            if (bytes[0] >= 0x80 && isStarted.load())
            {
                {
                    auto len = MidiMessage::getMessageLengthFromFirstByte (bytes[0]);
                    auto time = convertTimeStamp (timeStamp);
                    const ScopedLock sl (clientLock);

                    for (auto* c : clients)
                        c->pushMidiData (bytes, len, time);
                }

                writeFinishedBlocks();
            }
        }

        void handleSysEx (MIDIHDR* hdr, uint32 timeStamp)
        {
            if (isStarted.load() && hdr->dwBytesRecorded > 0)
            {
                {
                    auto time = convertTimeStamp (timeStamp);
                    const ScopedLock sl (clientLock);

                    for (auto* c : clients)
                        c->pushMidiData (hdr->lpData, (int) hdr->dwBytesRecorded, time);
                }

                writeFinishedBlocks();
            }
        }

        void startOrStop()
        {
            const ScopedLock sl (clientLock);

            if (countRunningClients() == 0)
                stop();
            else
                start();
        }

        void start()
        {
            if (deviceHandle != 0 && ! isStarted.load())
            {
                activeMidiCollectors.addIfNotAlreadyThere (this);

                for (int i = 0; i < (int) numHeaders; ++i)
                {
                    headers[i].prepare (deviceHandle);
                    headers[i].write (deviceHandle);
                }

                startTime = Time::getMillisecondCounterHiRes();
                auto res = midiInStart (deviceHandle);

                if (res == MMSYSERR_NOERROR)
                    isStarted = true;
                else
                    unprepareAllHeaders();
            }
        }

        void stop()
        {
            if (isStarted.load())
            {
                isStarted = false;
                midiInReset (deviceHandle);
                midiInStop (deviceHandle);
                activeMidiCollectors.removeFirstMatchingValue (this);
                unprepareAllHeaders();
            }
        }

        static void CALLBACK midiInCallback (HMIDIIN, UINT uMsg, DWORD_PTR dwInstance,
                                             DWORD_PTR midiMessage, DWORD_PTR timeStamp)
        {
            auto* collector = reinterpret_cast<MidiInCollector*> (dwInstance);

            // This is primarily a check for the collector being a dangling
            // pointer, as the callback can sometimes be delayed
            if (activeMidiCollectors.contains (collector))
            {
                if (uMsg == MIM_DATA)
                    collector->handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp);
                else if (uMsg == MIM_LONGDATA)
                    collector->handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp);
            }
        }

        String deviceName;
        HMIDIIN deviceHandle = 0;

    private:
        Win32MidiService& midiService;
        CriticalSection clientLock;
        Array<Win32InputWrapper*> clients;
        std::atomic<bool> isStarted { false };
        double startTime = 0;

        // This static array is used to prevent occasional callbacks to objects that are
        // in the process of being deleted
        static Array<MidiInCollector*, CriticalSection> activeMidiCollectors;

        int countRunningClients() const
        {
            int num = 0;

            for (auto* c : clients)
                if (c->started)
                    ++num;

            return num;
        }

        struct MidiHeader
        {
            MidiHeader() {}

            void prepare (HMIDIIN device)
            {
                zerostruct (hdr);
                hdr.lpData = data;
                hdr.dwBufferLength = (DWORD) numElementsInArray (data);

                midiInPrepareHeader (device, &hdr, sizeof (hdr));
            }

            void unprepare (HMIDIIN device)
            {
                if ((hdr.dwFlags & WHDR_DONE) != 0)
                {
                    int c = 10;
                    while (--c >= 0 && midiInUnprepareHeader (device, &hdr, sizeof (hdr)) == MIDIERR_STILLPLAYING)
                        Thread::sleep (20);

                    jassert (c >= 0);
                }
            }

            void write (HMIDIIN device)
            {
                hdr.dwBytesRecorded = 0;
                midiInAddBuffer (device, &hdr, sizeof (hdr));
            }

            void writeIfFinished (HMIDIIN device)
            {
                if ((hdr.dwFlags & WHDR_DONE) != 0)
                    write (device);
            }

            MIDIHDR hdr;
            char data[256];

            JUCE_DECLARE_NON_COPYABLE (MidiHeader)
        };

        enum { numHeaders = 32 };
        MidiHeader headers[numHeaders];

        void writeFinishedBlocks()
        {
            for (int i = 0; i < (int) numHeaders; ++i)
                headers[i].writeIfFinished (deviceHandle);
        }

        void unprepareAllHeaders()
        {
            for (int i = 0; i < (int) numHeaders; ++i)
                headers[i].unprepare (deviceHandle);
        }

        double convertTimeStamp (uint32 timeStamp)
        {
            auto t = startTime + timeStamp;
            auto now = Time::getMillisecondCounterHiRes();

            if (t > now)
            {
                if (t > now + 2.0)
                    startTime -= 1.0;

                t = now;
            }

            return t * 0.001;
        }

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInCollector)
    };

    //==============================================================================
    struct Win32InputWrapper  : public InputWrapper
    {
        Win32InputWrapper (Win32MidiService& parentService,
                           MidiInput& midiInput, int index, MidiInputCallback& c)
            : input (midiInput), callback (c)
        {
            collector = getOrCreateCollector (parentService, index);
            collector->addClient (this);
        }

        ~Win32InputWrapper()
        {
            collector->removeClient (this);
        }

        static MidiInCollector::Ptr getOrCreateCollector (Win32MidiService& parentService, int index)
        {
            auto names = getDevices();
            UINT deviceID = MIDI_MAPPER;
            String deviceName;

            if (isPositiveAndBelow (index, names.size()))
            {
                deviceName = names[index];
                deviceID = index;
            }

            const ScopedLock sl (parentService.activeCollectorLock);

            for (auto& c : parentService.activeCollectors)
                if (c->deviceName == deviceName)
                    return c;

            MidiInCollector::Ptr c (new MidiInCollector (parentService, deviceName));

            HMIDIIN h;
            auto err = midiInOpen (&h, deviceID,
                                   (DWORD_PTR) &MidiInCollector::midiInCallback,
                                   (DWORD_PTR) (MidiInCollector*) c.get(),
                                   CALLBACK_FUNCTION);

            if (err != MMSYSERR_NOERROR)
                throw std::runtime_error ("Failed to create Windows input device wrapper");

            c->deviceHandle = h;
            parentService.activeCollectors.add (c);
            return c;
        }

        static StringArray getDevices()
        {
            StringArray s;
            auto num = midiInGetNumDevs();

            for (UINT i = 0; i < num; ++i)
            {
                MIDIINCAPS mc = { 0 };

                if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR)
                    s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname)));
            }

            s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
            return s;
        }

        static int getDefaultDeviceIndex()  { return 0; }

        void start() override   { started = true;  concatenator.reset(); collector->startOrStop(); }
        void stop() override    { started = false; collector->startOrStop(); concatenator.reset(); }

        String getDeviceName() override     { return collector->deviceName; }

        void pushMidiData (const void* inputData, int numBytes, double time)
        {
            concatenator.pushMidiData (inputData, numBytes, time, &input, callback);
        }

        MidiInput& input;
        MidiInputCallback& callback;
        MidiDataConcatenator concatenator { 4096 };
        MidiInCollector::Ptr collector;
        bool started = false;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32InputWrapper)
    };

    //==============================================================================
    struct MidiOutHandle    : public ReferenceCountedObject
    {
        using Ptr = ReferenceCountedObjectPtr<MidiOutHandle>;

        MidiOutHandle (Win32MidiService& parent, const String& name, HMIDIOUT h)
            : owner (parent), deviceName (name), handle (h)
        {
            owner.activeOutputHandles.add (this);
        }

        ~MidiOutHandle()
        {
            if (handle != nullptr)
                midiOutClose (handle);

            owner.activeOutputHandles.removeFirstMatchingValue (this);
        }

        Win32MidiService& owner;
        String deviceName;
        HMIDIOUT handle;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutHandle)
    };

    //==============================================================================
    struct Win32OutputWrapper  : public OutputWrapper
    {
        Win32OutputWrapper (Win32MidiService& p, int index) : parent (p)
        {
            auto names = getDevices();
            UINT deviceID = MIDI_MAPPER;

            if (isPositiveAndBelow (index, names.size()))
            {
                deviceName = names[index];
                deviceID = index;
            }

            if (deviceID == MIDI_MAPPER)
            {
                // use the microsoft sw synth as a default - best not to allow deviceID
                // to be MIDI_MAPPER, or else device sharing breaks
                for (int i = 0; i < names.size(); ++i)
                    if (names[i].containsIgnoreCase ("microsoft"))
                        deviceID = (UINT) i;
            }

            for (int i = parent.activeOutputHandles.size(); --i >= 0;)
            {
                auto* activeHandle = parent.activeOutputHandles.getUnchecked (i);

                if (activeHandle->deviceName == deviceName)
                {
                    han = activeHandle;
                    return;
                }
            }

            for (int i = 4; --i >= 0;)
            {
                HMIDIOUT h = 0;
                auto res = midiOutOpen (&h, deviceID, 0, 0, CALLBACK_NULL);

                if (res == MMSYSERR_NOERROR)
                {
                    han = new MidiOutHandle (parent, deviceName, h);
                    return;
                }

                if (res == MMSYSERR_ALLOCATED)
                    Sleep (100);
                else
                    break;
            }

            throw std::runtime_error ("Failed to create Windows output device wrapper");
        }

        void sendMessageNow (const MidiMessage& message) override
        {
            if (message.getRawDataSize() > 3 || message.isSysEx())
            {
                MIDIHDR h = { 0 };

                h.lpData = (char*) message.getRawData();
                h.dwBytesRecorded = h.dwBufferLength  = (DWORD) message.getRawDataSize();

                if (midiOutPrepareHeader (han->handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR)
                {
                    auto res = midiOutLongMsg (han->handle, &h, sizeof (MIDIHDR));

                    if (res == MMSYSERR_NOERROR)
                    {
                        while ((h.dwFlags & MHDR_DONE) == 0)
                            Sleep (1);

                        int count = 500; // 1 sec timeout

                        while (--count >= 0)
                        {
                            res = midiOutUnprepareHeader (han->handle, &h, sizeof (MIDIHDR));

                            if (res == MIDIERR_STILLPLAYING)
                                Sleep (2);
                            else
                                break;
                        }
                    }
                }
            }
            else
            {
                for (int i = 0; i < 50; ++i)
                {
                    if (midiOutShortMsg (han->handle, *(unsigned int*) message.getRawData()) != MIDIERR_NOTREADY)
                        break;

                    Sleep (1);
                }
            }
        }

        static Array<MIDIOUTCAPS> getDeviceCaps()
        {
            Array<MIDIOUTCAPS> devices;
            auto num = midiOutGetNumDevs();

            for (UINT i = 0; i < num; ++i)
            {
                MIDIOUTCAPS mc = { 0 };

                if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR)
                    devices.add (mc);
            }

            return devices;
        }

        static StringArray getDevices()
        {
            StringArray s;

            for (auto& mc : getDeviceCaps())
                s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname)));

            s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
            return s;
        }

        static int getDefaultDeviceIndex()
        {
            int n = 0;

            for (auto& mc : getDeviceCaps())
            {
                if ((mc.wTechnology & MOD_MAPPER) != 0)
                    return n;

                ++n;
            }

            return 0;
        }

        String getDeviceName() override    { return deviceName; }

        Win32MidiService& parent;
        String deviceName;
        MidiOutHandle::Ptr han;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32OutputWrapper)
    };

    //==============================================================================
    void asyncCheckForUnusedCollectors()
    {
        startTimer (10);
    }

    void timerCallback() override
    {
        stopTimer();

        const ScopedLock sl (activeCollectorLock);

        for (int i = activeCollectors.size(); --i >= 0;)
            if (activeCollectors.getObjectPointer(i)->getReferenceCount() == 1)
                activeCollectors.remove (i);
    }

    CriticalSection activeCollectorLock;
    ReferenceCountedArray<MidiInCollector> activeCollectors;
    Array<MidiOutHandle*> activeOutputHandles;
};

Array<Win32MidiService::MidiInCollector*, CriticalSection> Win32MidiService::MidiInCollector::activeMidiCollectors;

//==============================================================================
//==============================================================================
#if JUCE_USE_WINRT_MIDI

#ifndef JUCE_FORCE_WINRT_MIDI
 #define JUCE_FORCE_WINRT_MIDI 0
#endif

#ifndef JUCE_WINRT_MIDI_LOGGING
 #define JUCE_WINRT_MIDI_LOGGING 0
#endif

#if JUCE_WINRT_MIDI_LOGGING
 #define JUCE_WINRT_MIDI_LOG(x)  DBG(x)
#else
 #define JUCE_WINRT_MIDI_LOG(x)
#endif

using namespace Microsoft::WRL;

using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Devices::Midi;
using namespace ABI::Windows::Devices::Enumeration;
using namespace ABI::Windows::Storage::Streams;

//==============================================================================
struct WinRTMidiService  : public MidiServiceType
{
public:
    //==============================================================================
    WinRTMidiService()
    {
        if (! WinRTWrapper::getInstance()->isInitialised())
            throw std::runtime_error ("Failed to initialise the WinRT wrapper");

        midiInFactory = WinRTWrapper::getInstance()->getWRLFactory<IMidiInPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]);

        if (midiInFactory == nullptr)
            throw std::runtime_error ("Failed to create midi in factory");

        midiOutFactory = WinRTWrapper::getInstance()->getWRLFactory<IMidiOutPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]);

        if (midiOutFactory == nullptr)
            throw std::runtime_error ("Failed to create midi out factory");

        // The WinRT BLE MIDI API doesn't provide callbacks when devices become disconnected,
        // but it does require a disconnection via the API before a device will reconnect again.
        // We can monitor the BLE connection state of paired devices to get callbacks when
        // connections are broken.
        bleDeviceWatcher.reset (new BLEDeviceWatcher());

        if (! bleDeviceWatcher->start())
            throw std::runtime_error ("Failed to start the BLE device watcher");

        inputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiInPortStatics> (midiInFactory));

        if (! inputDeviceWatcher->start())
            throw std::runtime_error ("Failed to start the midi input device watcher");

        outputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiOutPortStatics> (midiOutFactory));

        if (! outputDeviceWatcher->start())
            throw std::runtime_error ("Failed to start the midi output device watcher");
    }

    StringArray getDevices (bool isInput) override
    {
        return isInput ? inputDeviceWatcher ->getDevices()
                       : outputDeviceWatcher->getDevices();
    }

    int getDefaultDeviceIndex (bool isInput) override
    {
        return isInput ? inputDeviceWatcher ->getDefaultDeviceIndex()
                       : outputDeviceWatcher->getDefaultDeviceIndex();
    }

    InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override
    {
        return new WinRTInputWrapper (*this, input, index, callback);
    }

    OutputWrapper* createOutputWrapper (int index) override
    {
        return new WinRTOutputWrapper (*this, index);
    }

private:
    //==============================================================================
    class DeviceCallbackHandler
    {
    public:
        virtual ~DeviceCallbackHandler() {};

        virtual HRESULT addDevice (IDeviceInformation*) = 0;
        virtual HRESULT removeDevice (IDeviceInformationUpdate*) = 0;
        virtual HRESULT updateDevice (IDeviceInformationUpdate*) = 0;

        bool attach (HSTRING deviceSelector, DeviceInformationKind infoKind)
        {
            auto deviceInfoFactory = WinRTWrapper::getInstance()->getWRLFactory<IDeviceInformationStatics2> (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]);

            if (deviceInfoFactory == nullptr)
                return false;

            // A quick way of getting an IVector<HSTRING>...
            auto requestedProperties = []
            {
                auto devicePicker = WinRTWrapper::getInstance()->activateInstance<IDevicePicker> (&RuntimeClass_Windows_Devices_Enumeration_DevicePicker[0],
                                                                                                  __uuidof (IDevicePicker));
                jassert (devicePicker != nullptr);

                IVector<HSTRING>* result;
                auto hr = devicePicker->get_RequestedProperties (&result);
                jassert (SUCCEEDED (hr));

                hr = result->Clear();
                jassert (SUCCEEDED (hr));

                return result;
            }();

            StringArray propertyKeys = { "System.Devices.ContainerId",
                                         "System.Devices.Aep.ContainerId",
                                         "System.Devices.Aep.IsConnected" };

            for (auto& key : propertyKeys)
            {
                WinRTWrapper::ScopedHString hstr (key);
                auto hr = requestedProperties->Append (hstr.get());

                if (FAILED (hr))
                {
                    jassertfalse;
                    return false;
                }
            }

            WinRTWrapper::ComPtr<IIterable<HSTRING>> iter;
            auto hr = requestedProperties->QueryInterface (__uuidof (IIterable<HSTRING>), (void**) iter.resetAndGetPointerAddress());

            if (FAILED (hr))
            {
                jassertfalse;
                return false;
            }

            hr = deviceInfoFactory->CreateWatcherWithKindAqsFilterAndAdditionalProperties (deviceSelector, iter, infoKind,
                                                                                           watcher.resetAndGetPointerAddress());

            if (FAILED (hr))
            {
                jassertfalse;
                return false;
            }

            enumerationThread.startThread();

            return true;
        };

        void detach()
        {
            enumerationThread.stopThread (2000);

            if (watcher == nullptr)
                return;

            auto hr = watcher->Stop();
            jassert (SUCCEEDED (hr));

            if (deviceAddedToken.value != 0)
            {
                hr = watcher->remove_Added (deviceAddedToken);
                jassert (SUCCEEDED (hr));
                deviceAddedToken.value = 0;
            }

            if (deviceUpdatedToken.value != 0)
            {
                hr = watcher->remove_Updated (deviceUpdatedToken);
                jassert (SUCCEEDED (hr));
                deviceUpdatedToken.value = 0;
            }

            if (deviceRemovedToken.value != 0)
            {
                hr = watcher->remove_Removed (deviceRemovedToken);
                jassert (SUCCEEDED (hr));
                deviceRemovedToken.value = 0;
            }

            watcher = nullptr;
        }

        template<typename InfoType>
        IInspectable* getValueFromDeviceInfo (String key, InfoType* info)
        {
            __FIMapView_2_HSTRING_IInspectable* properties;
            info->get_Properties (&properties);

            boolean found = false;
            WinRTWrapper::ScopedHString keyHstr (key);
            auto hr = properties->HasKey (keyHstr.get(), &found);

            if (FAILED (hr))
            {
                jassertfalse;
                return nullptr;
            }

            if (! found)
                return nullptr;

            IInspectable* inspectable;
            hr = properties->Lookup (keyHstr.get(), &inspectable);

            if (FAILED (hr))
            {
                jassertfalse;
                return nullptr;
            }

            return inspectable;
        }

        String getGUIDFromInspectable (IInspectable& inspectable)
        {
            WinRTWrapper::ComPtr<IReference<GUID>> guidRef;
            auto hr = inspectable.QueryInterface (__uuidof (IReference<GUID>),
                                                  (void**) guidRef.resetAndGetPointerAddress());

            if (FAILED (hr))
            {
                jassertfalse;
                return {};
            }

            GUID result;
            hr = guidRef->get_Value (&result);

            if (FAILED (hr))
            {
                jassertfalse;
                return {};
            }

            OLECHAR* resultString;
            StringFromCLSID (result, &resultString);

            return resultString;
        }

        bool getBoolFromInspectable (IInspectable& inspectable)
        {
            WinRTWrapper::ComPtr<IReference<bool>> boolRef;
            auto hr = inspectable.QueryInterface (__uuidof (IReference<bool>),
                                                  (void**) boolRef.resetAndGetPointerAddress());

            if (FAILED (hr))
            {
                jassertfalse;
                return false;
            }

            boolean result;
            hr = boolRef->get_Value (&result);

            if (FAILED (hr))
            {
                jassertfalse;
                return false;
            }

            return result;
        }

    private:
        //==============================================================================
        struct DeviceEnumerationThread   : public Thread
        {
            DeviceEnumerationThread (DeviceCallbackHandler& h,
                                     WinRTWrapper::ComPtr<IDeviceWatcher>& w,
                                     EventRegistrationToken& added,
                                     EventRegistrationToken& removed,
                                     EventRegistrationToken& updated)
                    : Thread ("WinRT Device Enumeration Thread"), handler (h), watcher (w),
                      deviceAddedToken (added), deviceRemovedToken (removed), deviceUpdatedToken (updated)
            {}

            void run() override
            {
                auto handlerPtr = std::addressof (handler);

                watcher->add_Added (
                    Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>> (
                        [handlerPtr](IDeviceWatcher*, IDeviceInformation* info) { return handlerPtr->addDevice (info); }
                    ).Get(),
                    &deviceAddedToken);

                watcher->add_Removed (
                    Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
                        [handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->removeDevice (infoUpdate); }
                    ).Get(),
                    &deviceRemovedToken);

                watcher->add_Updated (
                    Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
                        [handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->updateDevice (infoUpdate); }
                    ).Get(),
                    &deviceRemovedToken);

                watcher->Start();
            }

            DeviceCallbackHandler& handler;
            WinRTWrapper::ComPtr<IDeviceWatcher>& watcher;
            EventRegistrationToken& deviceAddedToken, deviceRemovedToken, deviceUpdatedToken;
        };

        //==============================================================================
        WinRTWrapper::ComPtr<IDeviceWatcher> watcher;

        EventRegistrationToken deviceAddedToken   { 0 },
                               deviceRemovedToken { 0 },
                               deviceUpdatedToken { 0 };

        DeviceEnumerationThread enumerationThread { *this, watcher,
                                                    deviceAddedToken,
                                                    deviceRemovedToken,
                                                    deviceUpdatedToken };
    };

    //==============================================================================
    struct BLEDeviceWatcher final   : private DeviceCallbackHandler
    {
        struct DeviceInfo
        {
            String containerID;
            bool isConnected = false;
        };

        BLEDeviceWatcher() = default;

        ~BLEDeviceWatcher()
        {
            detach();
        }

        //==============================================================================
        HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override
        {
            HSTRING deviceIDHst;
            auto hr = addedDeviceInfo->get_Id (&deviceIDHst);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query added BLE device ID!");
                return S_OK;
            }

            auto deviceID = WinRTWrapper::getInstance()->hStringToString (deviceIDHst);
            JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID);

            if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", addedDeviceInfo))
            {
                auto containerID = getGUIDFromInspectable (*containerIDValue);

                if (containerID.isNotEmpty())
                {
                    DeviceInfo info = { containerID };

                    if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", addedDeviceInfo))
                        info.isConnected = getBoolFromInspectable (*connectedValue);

                    JUCE_WINRT_MIDI_LOG ("Adding BLE device: " << deviceID << " " << info.containerID
                                         << " " << (info.isConnected ? "connected" : "disconnected"));
                    devices.set (deviceID, info);

                    return S_OK;
                }
            }

            JUCE_WINRT_MIDI_LOG ("Failed to get a container ID for BLE device: " << deviceID);
            return S_OK;
        }

        HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override
        {
            HSTRING removedDeviceIdHstr;
            auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query removed BLE device ID!");
                return S_OK;
            }

            auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr);

            JUCE_WINRT_MIDI_LOG ("Removing BLE device: " << removedDeviceId);

            {
                const ScopedLock lock (deviceChanges);

                if (devices.contains (removedDeviceId))
                {
                    auto& info = devices.getReference (removedDeviceId);
                    listeners.call ([&info](Listener& l) { l.bleDeviceDisconnected (info.containerID); });
                    devices.remove (removedDeviceId);
                    JUCE_WINRT_MIDI_LOG ("Removed BLE device: " << removedDeviceId);
                }
            }

            return S_OK;
        }

        HRESULT updateDevice (IDeviceInformationUpdate* updatedDeviceInfo) override
        {
            HSTRING updatedDeviceIdHstr;
            auto hr = updatedDeviceInfo->get_Id (&updatedDeviceIdHstr);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query updated BLE device ID!");
                return S_OK;
            }

            auto updatedDeviceId = WinRTWrapper::getInstance()->hStringToString (updatedDeviceIdHstr);

            JUCE_WINRT_MIDI_LOG ("Updating BLE device: " << updatedDeviceId);

            if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", updatedDeviceInfo))
            {
                auto isConnected = getBoolFromInspectable (*connectedValue);

                {
                    const ScopedLock lock (deviceChanges);

                    if (! devices.contains (updatedDeviceId))
                        return S_OK;

                    auto& info = devices.getReference (updatedDeviceId);

                    if (info.isConnected && ! isConnected)
                    {
                        JUCE_WINRT_MIDI_LOG ("BLE device connection status change: " << updatedDeviceId << " " << info.containerID << " " << (isConnected ? "connected" : "disconnected"));
                        listeners.call ([&info](Listener& l) { l.bleDeviceDisconnected (info.containerID); });
                    }

                    info.isConnected = isConnected;
                }
            }

            return S_OK;
        }

        //==============================================================================
        bool start()
        {
            WinRTWrapper::ScopedHString deviceSelector ("System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\""
                                                        " AND System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True");
            return attach (deviceSelector.get(), DeviceInformationKind::DeviceInformationKind_AssociationEndpoint);
        }

        //==============================================================================
        struct Listener
        {
            virtual ~Listener() {};
            virtual void bleDeviceAdded (const String& containerID) = 0;
            virtual void bleDeviceDisconnected (const String& containerID) = 0;
        };

        void addListener (Listener* l)
        {
            listeners.add (l);
        }

        void removeListener (Listener* l)
        {
            listeners.remove (l);
        }

        //==============================================================================
        ListenerList<Listener> listeners;
        HashMap<String, DeviceInfo> devices;
        CriticalSection deviceChanges;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher);
    };

    //==============================================================================
    struct MIDIDeviceInfo
    {
        String deviceID, containerID, name;
        bool isDefault = false;
    };

    //==============================================================================
    template <typename COMFactoryType>
    struct MidiIODeviceWatcher final   : private DeviceCallbackHandler
    {
        MidiIODeviceWatcher (WinRTWrapper::ComPtr<COMFactoryType>& comFactory)
            : factory (comFactory)
        {
        }

        ~MidiIODeviceWatcher()
        {
            detach();
        }

        HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override
        {
            MIDIDeviceInfo info;

            HSTRING deviceID;
            auto hr = addedDeviceInfo->get_Id (&deviceID);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query added MIDI device ID!");
                return S_OK;
            }

            info.deviceID = WinRTWrapper::getInstance()->hStringToString (deviceID);

            JUCE_WINRT_MIDI_LOG ("Detected MIDI device: " << info.deviceID);

            boolean isEnabled = false;
            hr = addedDeviceInfo->get_IsEnabled (&isEnabled);

            if (FAILED (hr) || ! isEnabled)
            {
                JUCE_WINRT_MIDI_LOG ("MIDI device not enabled: " << info.deviceID);
                return S_OK;
            }

            // We use the container ID to match a MIDI device with a generic BLE device, if possible
            if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.ContainerId", addedDeviceInfo))
                info.containerID = getGUIDFromInspectable (*containerIDValue);

            HSTRING name;
            hr = addedDeviceInfo->get_Name (&name);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device name for " << info.deviceID);
                return S_OK;
            }

            info.name = WinRTWrapper::getInstance()->hStringToString (name);

            boolean isDefault = false;
            hr = addedDeviceInfo->get_IsDefault (&isDefault);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device defaultness for " << info.deviceID << " " << info.name);
                return S_OK;
            }

            info.isDefault = isDefault;

            JUCE_WINRT_MIDI_LOG ("Adding MIDI device: " << info.deviceID << " " << info.containerID << " " << info.name);

            {
                const ScopedLock lock (deviceChanges);
                connectedDevices.add (info);
            }

            return S_OK;
        }

        HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override
        {
            HSTRING removedDeviceIdHstr;
            auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!");
                return S_OK;
            }

            auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr);

            JUCE_WINRT_MIDI_LOG ("Removing MIDI device: " << removedDeviceId);

            {
                const ScopedLock lock (deviceChanges);

                for (int i = 0; i < connectedDevices.size(); ++i)
                {
                    if (connectedDevices[i].deviceID == removedDeviceId)
                    {
                        connectedDevices.remove (i);
                        JUCE_WINRT_MIDI_LOG ("Removed MIDI device: " << removedDeviceId);
                        break;
                    }
                }
            }

            return S_OK;
        }

        // This is never called
        HRESULT updateDevice (IDeviceInformationUpdate*) override   { return S_OK; }

        bool start()
        {
            HSTRING deviceSelector;
            auto hr = factory->GetDeviceSelector (&deviceSelector);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to get MIDI device selector!");
                return false;
            }

            return attach (deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface);
        }

        StringArray getDevices()
        {
            {
                const ScopedLock lock (deviceChanges);
                lastQueriedConnectedDevices = connectedDevices;
            }

            StringArray result;

            for (auto info : lastQueriedConnectedDevices.get())
                result.add (info.name);

            return result;
        }

        int getDefaultDeviceIndex()
        {
            auto& lastDevices = lastQueriedConnectedDevices.get();

            for (int i = 0; i < lastDevices.size(); ++i)
                if (lastDevices[i].isDefault)
                    return i;

            return 0;
        }

        MIDIDeviceInfo getDeviceInfoFromIndex (int index)
        {
            if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size()))
                return lastQueriedConnectedDevices.get()[index];

            return {};
        }

        String getDeviceID (const String& name)
        {
            const ScopedLock lock (deviceChanges);

            for (auto info : connectedDevices)
                if (info.name == name)
                    return info.deviceID;

            return {};
        }

        WinRTWrapper::ComPtr<COMFactoryType>& factory;

        Array<MIDIDeviceInfo> connectedDevices;
        CriticalSection deviceChanges;
        ThreadLocalValue<Array<MIDIDeviceInfo>> lastQueriedConnectedDevices;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher);
    };

    //==============================================================================
    template <typename COMFactoryType, typename COMInterfaceType, typename COMType>
    struct OpenMidiPortThread  : public Thread
    {
        OpenMidiPortThread (String threadName, String midiDeviceID,
                            WinRTWrapper::ComPtr<COMFactoryType>& comFactory,
                            WinRTWrapper::ComPtr<COMInterfaceType>& comPort)
            : Thread (threadName),
              deviceID (midiDeviceID),
              factory (comFactory),
              port (comPort)
        {
        }

        ~OpenMidiPortThread()
        {
            stopThread (2000);
        }

        void run() override
        {
            WinRTWrapper::ScopedHString hDeviceId (deviceID);
            WinRTWrapper::ComPtr<IAsyncOperation<COMType*>> asyncOp;
            auto hr = factory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress());

            if (FAILED (hr))
                return;

            hr = asyncOp->put_Completed (Callback<IAsyncOperationCompletedHandler<COMType*>> (
                [this] (IAsyncOperation<COMType*>* asyncOpPtr, AsyncStatus)
                {
                    if (asyncOpPtr == nullptr)
                        return E_ABORT;

                    auto hr = asyncOpPtr->GetResults (port.resetAndGetPointerAddress());

                    if (FAILED (hr))
                        return hr;

                    portOpened.signal();
                    return S_OK;
                }
            ).Get());

            // We need to use a timout here, rather than waiting indefinitely, as the
            // WinRT API can occaisonally hang!
            portOpened.wait (2000);
        }

        const String deviceID;
        WinRTWrapper::ComPtr<COMFactoryType>& factory;
        WinRTWrapper::ComPtr<COMInterfaceType>& port;
        WaitableEvent portOpened { true };
    };

    //==============================================================================
    template <typename MIDIIOStaticsType, typename MIDIPort>
    class WinRTIOWrapper   : private BLEDeviceWatcher::Listener
    {
    public:
        WinRTIOWrapper (BLEDeviceWatcher& bleWatcher,
                        MidiIODeviceWatcher<MIDIIOStaticsType>& midiDeviceWatcher,
                        int index)
            : bleDeviceWatcher (bleWatcher)
        {
            {
                const ScopedLock lock (midiDeviceWatcher.deviceChanges);
                deviceInfo = midiDeviceWatcher.getDeviceInfoFromIndex (index);
            }

            if (deviceInfo.deviceID.isEmpty())
                throw std::runtime_error ("Invalid device index");

            JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID);

            if (deviceInfo.containerID.isNotEmpty())
            {
                bleDeviceWatcher.addListener (this);

                const ScopedLock lock (bleDeviceWatcher.deviceChanges);

                HashMap<String, BLEDeviceWatcher::DeviceInfo>::Iterator iter (bleDeviceWatcher.devices);

                while (iter.next())
                {
                    if (iter.getValue().containerID == deviceInfo.containerID)
                    {
                        isBLEDevice = true;
                        break;
                    }
                }
            }
        }

        virtual ~WinRTIOWrapper()
        {
            bleDeviceWatcher.removeListener (this);

            disconnect();
        }

        //==============================================================================
        virtual void disconnect()
        {
            if (midiPort != nullptr)
            {
                if (isBLEDevice)
                    midiPort->Release();
            }

            midiPort = nullptr;
        }

    private:
        //==============================================================================
        void bleDeviceAdded (const String& containerID) override
        {
            if (containerID == deviceInfo.containerID)
                isBLEDevice = true;
        }

        void bleDeviceDisconnected (const String& containerID) override
        {
            if (containerID == deviceInfo.containerID)
            {
                JUCE_WINRT_MIDI_LOG ("Disconnecting MIDI port from BLE disconnection: " << deviceInfo.deviceID
                                     << " " << deviceInfo.containerID << " " << deviceInfo.name);
                disconnect();
            }
        }

    protected:
        //==============================================================================
        BLEDeviceWatcher& bleDeviceWatcher;
        MIDIDeviceInfo deviceInfo;
        bool isBLEDevice = false;
        WinRTWrapper::ComPtr<MIDIPort> midiPort;
    };

    //==============================================================================
    struct WinRTInputWrapper final  : public InputWrapper,
                                      private WinRTIOWrapper<IMidiInPortStatics, IMidiInPort>

    {
        WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb)
            : WinRTIOWrapper <IMidiInPortStatics, IMidiInPort> (*service.bleDeviceWatcher, *service.inputDeviceWatcher, index),
              inputDevice (input),
              callback (cb)
        {
            OpenMidiPortThread<IMidiInPortStatics, IMidiInPort, MidiInPort> portThread ("Open WinRT MIDI input port",
                                                                                        deviceInfo.deviceID,
                                                                                        service.midiInFactory,
                                                                                        midiPort);
            portThread.startThread();
            portThread.waitForThreadToExit (-1);

            if (midiPort == nullptr)
            {
                JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation");
                return;
            }

            startTime = Time::getMillisecondCounterHiRes();

            auto hr = midiPort->add_MessageReceived (
                Callback<ITypedEventHandler<MidiInPort*, MidiMessageReceivedEventArgs*>> (
                    [this](IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); }
                ).Get(),
                &midiInMessageToken);

            if (FAILED (hr))
            {
                JUCE_WINRT_MIDI_LOG ("Failed to set MIDI input callback");
                jassertfalse;
            }
        }

        ~WinRTInputWrapper()
        {
            disconnect();
        }

        //==============================================================================
        void start() override
        {
            if (! isStarted)
            {
                concatenator.reset();
                isStarted = true;
            }
        }

        void stop() override
        {
            if (isStarted)
            {
                isStarted = false;
                concatenator.reset();
            }
        }

        String getDeviceName() override         { return deviceInfo.name; }

        //==============================================================================
        void disconnect() override
        {
            stop();

            if (midiPort != nullptr && midiInMessageToken.value != 0)
                midiPort->remove_MessageReceived (midiInMessageToken);

            WinRTIOWrapper<IMidiInPortStatics, IMidiInPort>::disconnect();
        }

        //==============================================================================
        HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args)
        {
            if (! isStarted)
                return S_OK;

            WinRTWrapper::ComPtr<IMidiMessage> message;
            auto hr = args->get_Message (message.resetAndGetPointerAddress());

            if (FAILED (hr))
                return hr;

            WinRTWrapper::ComPtr<IBuffer> buffer;
            hr = message->get_RawData (buffer.resetAndGetPointerAddress());

            if (FAILED (hr))
                return hr;

            WinRTWrapper::ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
            hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress());

            if (FAILED (hr))
                return hr;

            uint8_t* bufferData = nullptr;
            hr = bufferByteAccess->Buffer (&bufferData);

            if (FAILED (hr))
                return hr;

            uint32_t numBytes = 0;
            hr = buffer->get_Length (&numBytes);

            if (FAILED (hr))
                return hr;

            ABI::Windows::Foundation::TimeSpan timespan;
            hr = message->get_Timestamp (&timespan);

            if (FAILED (hr))
                return hr;

            concatenator.pushMidiData (bufferData, numBytes,
                                       convertTimeStamp (timespan.Duration),
                                       &inputDevice, callback);
            return S_OK;
        }

        double convertTimeStamp (int64 timestamp)
        {
            auto millisecondsSinceStart = static_cast<double> (timestamp) / 10000.0;
            auto t = startTime + millisecondsSinceStart;
            auto now = Time::getMillisecondCounterHiRes();

            if (t > now)
            {
                if (t > now + 2.0)
                    startTime -= 1.0;

                t = now;
            }

            return t * 0.001;
        }

        //==============================================================================
        MidiInput& inputDevice;
        MidiInputCallback& callback;

        MidiDataConcatenator concatenator { 4096 };
        EventRegistrationToken midiInMessageToken { 0 };

        double startTime = 0;
        bool isStarted = false;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTInputWrapper);
    };

    //==============================================================================
    struct WinRTOutputWrapper final  : public OutputWrapper,
                                       private WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort>
    {
        WinRTOutputWrapper (WinRTMidiService& service, int index)
            : WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort> (*service.bleDeviceWatcher, *service.outputDeviceWatcher, index)
        {
            OpenMidiPortThread<IMidiOutPortStatics, IMidiOutPort, IMidiOutPort> portThread ("Open WinRT MIDI output port",
                                                                                            deviceInfo.deviceID,
                                                                                            service.midiOutFactory,
                                                                                            midiPort);
            portThread.startThread();
            portThread.waitForThreadToExit (-1);

            if (midiPort == nullptr)
                throw std::runtime_error ("Timed out waiting for midi output port creation");

            auto bufferFactory = WinRTWrapper::getInstance()->getWRLFactory<IBufferFactory> (&RuntimeClass_Windows_Storage_Streams_Buffer[0]);

            if (bufferFactory == nullptr)
                throw std::runtime_error ("Failed to create output buffer factory");

            auto hr = bufferFactory->Create (static_cast<UINT32> (65536), buffer.resetAndGetPointerAddress());

            if (FAILED (hr))
                throw std::runtime_error ("Failed to create output buffer");

            hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress());

            if (FAILED (hr))
                throw std::runtime_error ("Failed to get buffer byte access");

            hr = bufferByteAccess->Buffer (&bufferData);

            if (FAILED (hr))
                throw std::runtime_error ("Failed to get buffer data pointer");
        }

        //==============================================================================
        void sendMessageNow (const MidiMessage& message) override
        {
            if (midiPort == nullptr)
                return;

            auto numBytes = message.getRawDataSize();
            auto hr = buffer->put_Length (numBytes);

            if (FAILED (hr))
            {
                jassertfalse;
                return;
            }

            memcpy_s (bufferData, numBytes, message.getRawData(), numBytes);
            midiPort->SendBuffer (buffer);
        }

        String getDeviceName() override    { return deviceInfo.name; }

        //==============================================================================
        WinRTWrapper::ComPtr<IBuffer> buffer;
        WinRTWrapper::ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
        uint8_t* bufferData = nullptr;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTOutputWrapper);
    };

    WinRTWrapper::ComPtr<IMidiInPortStatics>  midiInFactory;
    WinRTWrapper::ComPtr<IMidiOutPortStatics> midiOutFactory;

    std::unique_ptr<MidiIODeviceWatcher<IMidiInPortStatics>>  inputDeviceWatcher;
    std::unique_ptr<MidiIODeviceWatcher<IMidiOutPortStatics>> outputDeviceWatcher;
    std::unique_ptr<BLEDeviceWatcher> bleDeviceWatcher;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService)
};

#endif   // JUCE_USE_WINRT_MIDI

//==============================================================================
//==============================================================================
struct MidiService :  public DeletedAtShutdown
{
    MidiService()
    {
      #if JUCE_USE_WINRT_MIDI
       #if ! JUCE_FORCE_WINRT_MIDI
        auto windowsVersionInfo = []
        {
            RTL_OSVERSIONINFOW versionInfo = { 0 };

            if (auto* mod = ::GetModuleHandleW (L"ntdll.dll"))
            {
                using RtlGetVersion = LONG (WINAPI*)(PRTL_OSVERSIONINFOW);

                if (auto* rtlGetVersion = (RtlGetVersion) ::GetProcAddress (mod, "RtlGetVersion"))
                {
                    versionInfo.dwOSVersionInfoSize = sizeof (versionInfo);
                    LONG STATUS_SUCCESS = 0;

                    if (rtlGetVersion (&versionInfo) != STATUS_SUCCESS)
                        versionInfo = { 0 };
                }
            }

            return versionInfo;
        }();

        if (windowsVersionInfo.dwMajorVersion >= 10 && windowsVersionInfo.dwBuildNumber >= 17763)
       #endif
        {
            try
            {
                internal.reset (new WinRTMidiService());
                return;
            }
            catch (std::runtime_error&) {}
        }
      #endif

        internal.reset (new Win32MidiService());
    }

    ~MidiService()
    {
        clearSingletonInstance();
    }

    static MidiServiceType& getService()
    {
        jassert (getInstance()->internal != nullptr);
        return *getInstance()->internal.get();
    }

    JUCE_DECLARE_SINGLETON (MidiService, false)

private:
    std::unique_ptr<MidiServiceType> internal;
};

JUCE_IMPLEMENT_SINGLETON (MidiService)

//==============================================================================
StringArray MidiInput::getDevices()
{
    return MidiService::getService().getDevices (true);
}

int MidiInput::getDefaultDeviceIndex()
{
    return MidiService::getService().getDefaultDeviceIndex (true);
}

MidiInput::MidiInput (const String& deviceName)  : name (deviceName)
{
}

MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback)
{
    if (callback == nullptr)
        return nullptr;

    std::unique_ptr<MidiInput> in (new MidiInput (String()));
    std::unique_ptr<MidiServiceType::InputWrapper> wrapper;

    try
    {
        wrapper.reset (MidiService::getService().createInputWrapper (*in, index, *callback));
    }
    catch (std::runtime_error&)
    {
        return nullptr;
    }

    in->setName (wrapper->getDeviceName());
    in->internal = wrapper.release();
    return in.release();
}

MidiInput::~MidiInput()
{
    delete static_cast<MidiServiceType::InputWrapper*> (internal);
}

void MidiInput::start()   { static_cast<MidiServiceType::InputWrapper*> (internal)->start(); }
void MidiInput::stop()    { static_cast<MidiServiceType::InputWrapper*> (internal)->stop(); }

//==============================================================================
StringArray MidiOutput::getDevices()
{
    return MidiService::getService().getDevices (false);
}

int MidiOutput::getDefaultDeviceIndex()
{
    return MidiService::getService().getDefaultDeviceIndex (false);
}

MidiOutput* MidiOutput::openDevice (int index)
{
    std::unique_ptr<MidiServiceType::OutputWrapper> wrapper;

    try
    {
        wrapper.reset (MidiService::getService().createOutputWrapper (index));
    }
    catch (std::runtime_error&)
    {
        return nullptr;
    }

    std::unique_ptr<MidiOutput> out (new MidiOutput (wrapper->getDeviceName()));
    out->internal = wrapper.release();
    return out.release();
}

MidiOutput::~MidiOutput()
{
    stopBackgroundThread();
    delete static_cast<MidiServiceType::OutputWrapper*> (internal);
}

void MidiOutput::sendMessageNow (const MidiMessage& message)
{
    static_cast<MidiServiceType::OutputWrapper*> (internal)->sendMessageNow (message);
}

} // namespace juce
