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

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-9-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

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

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

namespace juce
{

namespace
{

#ifndef JUCE_ALSA_LOGGING
 #define JUCE_ALSA_LOGGING 0
#endif

#if JUCE_ALSA_LOGGING
 #define JUCE_ALSA_LOG(dbgtext)   { juce::String tempDbgBuf ("ALSA: "); tempDbgBuf << dbgtext; Logger::writeToLog (tempDbgBuf); DBG (tempDbgBuf); }
 #define JUCE_CHECKED_RESULT(x)   (logErrorMessage (x, __LINE__))

 static int logErrorMessage (int err, int lineNum)
 {
    if (err < 0)
        JUCE_ALSA_LOG ("Error: line " << lineNum << ": code " << err << " (" << snd_strerror (err) << ")");

    return err;
 }
#else
 #define JUCE_ALSA_LOG(x)         {}
 #define JUCE_CHECKED_RESULT(x)   (x)
#endif

#define JUCE_ALSA_FAILED(x)  failed (x)

static void getDeviceSampleRates (snd_pcm_t* handle, Array<double>& rates)
{
    snd_pcm_hw_params_t* hwParams;
    snd_pcm_hw_params_alloca (&hwParams);

    for (const auto rateToTry : SampleRateHelpers::getCommonSampleRates())
    {
        if (snd_pcm_hw_params_any (handle, hwParams) >= 0
             && snd_pcm_hw_params_test_rate (handle, hwParams, (unsigned int) rateToTry, 0) == 0)
        {
            rates.addIfNotAlreadyThere (rateToTry);
        }
    }
}

static void getDeviceNumChannels (snd_pcm_t* handle, unsigned int* minChans, unsigned int* maxChans)
{
    snd_pcm_hw_params_t *params;
    snd_pcm_hw_params_alloca (&params);

    if (snd_pcm_hw_params_any (handle, params) >= 0)
    {
        snd_pcm_hw_params_get_channels_min (params, minChans);
        snd_pcm_hw_params_get_channels_max (params, maxChans);

        JUCE_ALSA_LOG ("getDeviceNumChannels: " << (int) *minChans << " " << (int) *maxChans);

        // some virtual devices (dmix for example) report 10000 channels , we have to clamp these values
        *maxChans = jmin (*maxChans, 256u);
        *minChans = jmin (*minChans, *maxChans);
    }
    else
    {
        JUCE_ALSA_LOG ("getDeviceNumChannels failed");
    }
}

static void getDeviceProperties (const String& deviceID,
                                 unsigned int& minChansOut,
                                 unsigned int& maxChansOut,
                                 unsigned int& minChansIn,
                                 unsigned int& maxChansIn,
                                 Array<double>& rates,
                                 bool testOutput,
                                 bool testInput)
{
    minChansOut = maxChansOut = minChansIn = maxChansIn = 0;

    if (deviceID.isEmpty())
        return;

    JUCE_ALSA_LOG ("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")");

    snd_pcm_info_t* info;
    snd_pcm_info_alloca (&info);

    if (testOutput)
    {
        snd_pcm_t* pcmHandle;

        if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8().getAddress(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) >= 0)
        {
            getDeviceNumChannels (pcmHandle, &minChansOut, &maxChansOut);
            getDeviceSampleRates (pcmHandle, rates);

            snd_pcm_close (pcmHandle);
        }
    }

    if (testInput)
    {
        snd_pcm_t* pcmHandle;

        if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK) >= 0))
        {
            getDeviceNumChannels (pcmHandle, &minChansIn, &maxChansIn);

            if (rates.size() == 0)
                getDeviceSampleRates (pcmHandle, rates);

            snd_pcm_close (pcmHandle);
        }
    }
}

static void ensureMinimumNumBitsSet (BigInteger& chans, int minNumChans)
{
    int i = 0;

    while (chans.countNumberOfSetBits() < minNumChans)
        chans.setBit (i++);
}

static void silentErrorHandler (const char*, int, const char*, int, const char*,...) {}

//==============================================================================
class ALSADevice
{
public:
    ALSADevice (const String& devID, bool forInput)
        : handle (nullptr),
          bitDepth (16),
          numChannelsRunning (0),
          latency (0),
          deviceID (devID),
          isInput (forInput),
          isInterleaved (true)
    {
        JUCE_ALSA_LOG ("snd_pcm_open (" << deviceID.toUTF8().getAddress() << ", forInput=" << (int) forInput << ")");

        int err = snd_pcm_open (&handle, deviceID.toUTF8(),
                                forInput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
                                SND_PCM_ASYNC);
        if (err < 0)
        {
            if (-err == EBUSY)
                error << "The device \"" << deviceID << "\" is busy (another application is using it).";
            else if (-err == ENOENT)
                error << "The device \"" << deviceID << "\" is not available.";
            else
                error << "Could not open " << (forInput ? "input" : "output") << " device \"" << deviceID
                      << "\": " << snd_strerror (err) << " (" << err << ")";

            JUCE_ALSA_LOG ("snd_pcm_open failed; " << error);
        }
    }

    ~ALSADevice()
    {
        closeNow();
    }

    void closeNow()
    {
        if (handle != nullptr)
        {
            snd_pcm_close (handle);
            handle = nullptr;
        }
    }

    bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize)
    {
        if (handle == nullptr)
            return false;

        JUCE_ALSA_LOG ("ALSADevice::setParameters(" << deviceID << ", "
                         << (int) sampleRate << ", " << numChannels << ", " << bufferSize << ")");

        snd_pcm_hw_params_t* hwParams;
        snd_pcm_hw_params_alloca (&hwParams);

        if (snd_pcm_hw_params_any (handle, hwParams) < 0)
        {
            // this is the error message that aplay returns when an error happens here,
            // it is a bit more explicit that "Invalid parameter"
            error = "Broken configuration for this PCM: no configurations available";
            return false;
        }

        if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED) >= 0) // works better for plughw
            isInterleaved = true;
        else if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_NONINTERLEAVED) >= 0)
            isInterleaved = false;
        else
        {
            jassertfalse;
            return false;
        }

        enum { isFloatBit = 1 << 16, isLittleEndianBit = 1 << 17, onlyUseLower24Bits = 1 << 18 };

        const int formatsToTry[] = { SND_PCM_FORMAT_FLOAT_LE,   32 | isFloatBit | isLittleEndianBit,
                                     SND_PCM_FORMAT_FLOAT_BE,   32 | isFloatBit,
                                     SND_PCM_FORMAT_S32_LE,     32 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S32_BE,     32,
                                     SND_PCM_FORMAT_S24_3LE,    24 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S24_3BE,    24,
                                     SND_PCM_FORMAT_S24_LE,     32 | isLittleEndianBit | onlyUseLower24Bits,
                                     SND_PCM_FORMAT_S16_LE,     16 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S16_BE,     16 };
        bitDepth = 0;

        for (int i = 0; i < numElementsInArray (formatsToTry); i += 2)
        {
            if (snd_pcm_hw_params_set_format (handle, hwParams, (_snd_pcm_format) formatsToTry [i]) >= 0)
            {
                const int type = formatsToTry [i + 1];
                bitDepth = type & 255;

                converter.reset (createConverter (isInput, bitDepth,
                                                  (type & isFloatBit) != 0,
                                                  (type & isLittleEndianBit) != 0,
                                                  (type & onlyUseLower24Bits) != 0,
                                                  numChannels,
                                                  isInterleaved));
                break;
            }
        }

        if (bitDepth == 0)
        {
            error = "device doesn't support a compatible PCM format";
            JUCE_ALSA_LOG ("Error: " + error);
            return false;
        }

        int dir = 0;
        unsigned int periods = 4;
        snd_pcm_uframes_t samplesPerPeriod = (snd_pcm_uframes_t) bufferSize;

        if (JUCE_ALSA_FAILED (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, nullptr))
            || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_channels (handle, hwParams, (unsigned int ) numChannels))
            || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir))
            || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir))
            || JUCE_ALSA_FAILED (snd_pcm_hw_params (handle, hwParams)))
        {
            return false;
        }

        snd_pcm_uframes_t frames = 0;

        if (JUCE_ALSA_FAILED (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir))
             || JUCE_ALSA_FAILED (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir)))
            latency = 0;
        else
            latency = (int) frames * ((int) periods - 1); // (this is the method JACK uses to guess the latency)

        JUCE_ALSA_LOG ("frames: " << (int) frames << ", periods: " << (int) periods
                          << ", samplesPerPeriod: " << (int) samplesPerPeriod);

        snd_pcm_sw_params_t* swParams;
        snd_pcm_sw_params_alloca (&swParams);
        snd_pcm_uframes_t boundary;

        if (JUCE_ALSA_FAILED (snd_pcm_sw_params_current (handle, swParams))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params_get_boundary (swParams, &boundary))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_threshold (handle, swParams, 0))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_size (handle, swParams, boundary))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_start_threshold (handle, swParams, samplesPerPeriod))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_stop_threshold (handle, swParams, boundary))
            || JUCE_ALSA_FAILED (snd_pcm_sw_params (handle, swParams)))
        {
            return false;
        }

       #if JUCE_ALSA_LOGGING
        // enable this to dump the config of the devices that get opened
        snd_output_t* out;
        snd_output_stdio_attach (&out, stderr, 0);
        snd_pcm_hw_params_dump (hwParams, out);
        snd_pcm_sw_params_dump (swParams, out);
       #endif

        numChannelsRunning = numChannels;

        return true;
    }

    //==============================================================================
    bool writeToOutputDevice (AudioBuffer<float>& outputChannelBuffer, const int numSamples)
    {
        jassert (numChannelsRunning <= outputChannelBuffer.getNumChannels());
        float* const* const data = outputChannelBuffer.getArrayOfWritePointers();
        snd_pcm_sframes_t numDone = 0;

        if (isInterleaved)
        {
            scratch.ensureSize ((size_t) ((int) sizeof (float) * numSamples * numChannelsRunning), false);

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (scratch.getData(), i, data[i], 0, numSamples);

            numDone = snd_pcm_writei (handle, scratch.getData(), (snd_pcm_uframes_t) numSamples);
        }
        else
        {
            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], data[i], numSamples);

            numDone = snd_pcm_writen (handle, (void**) data, (snd_pcm_uframes_t) numSamples);
        }

        if (numDone < 0)
        {
            if (numDone == -(EPIPE))
                underrunCount++;

            if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) numDone, 1 /* silent */)))
                return false;
        }

        if (numDone < numSamples)
            JUCE_ALSA_LOG ("Did not write all samples: numDone: " << numDone << ", numSamples: " << numSamples);

        return true;
    }

    bool readFromInputDevice (AudioBuffer<float>& inputChannelBuffer, const int numSamples)
    {
        jassert (numChannelsRunning <= inputChannelBuffer.getNumChannels());
        float* const* const data = inputChannelBuffer.getArrayOfWritePointers();

        if (isInterleaved)
        {
            scratch.ensureSize ((size_t) ((int) sizeof (float) * numSamples * numChannelsRunning), false);
            scratch.fillWith (0); // (not clearing this data causes warnings in valgrind)

            auto num = snd_pcm_readi (handle, scratch.getData(), (snd_pcm_uframes_t) numSamples);

            if (num < 0)
            {
                if (num == -(EPIPE))
                    overrunCount++;

                if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) num, 1 /* silent */)))
                    return false;
            }


            if (num < numSamples)
                JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples);

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], 0, scratch.getData(), i, numSamples);
        }
        else
        {
            auto num = snd_pcm_readn (handle, (void**) data, (snd_pcm_uframes_t) numSamples);

            if (num < 0)
            {
                if (num == -(EPIPE))
                    overrunCount++;

                if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) num, 1 /* silent */)))
                    return false;
            }

            if (num < numSamples)
                JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples);

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], data[i], numSamples);
        }

        return true;
    }

    //==============================================================================
    snd_pcm_t* handle;
    String error;
    int bitDepth, numChannelsRunning, latency;
    int underrunCount = 0, overrunCount = 0;

private:
    //==============================================================================
    String deviceID;
    const bool isInput;
    bool isInterleaved;
    MemoryBlock scratch;
    std::unique_ptr<AudioData::Converter> converter;

    //==============================================================================
    template <class SampleType>
    struct ConverterHelper
    {
        static AudioData::Converter* createConverter (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels, bool interleaved)
        {
            if (interleaved)
                return create<AudioData::Interleaved> (forInput, isLittleEndian, numInterleavedChannels);

            return create<AudioData::NonInterleaved> (forInput, isLittleEndian, numInterleavedChannels);
        }

    private:
        template <class InterleavedType>
        static AudioData::Converter* create (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels)
        {
            if (forInput)
            {
                using DestType = AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst>;

                if (isLittleEndian)
                    return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::LittleEndian, InterleavedType, AudioData::Const>, DestType> (numInterleavedChannels, 1);

                return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::BigEndian, InterleavedType, AudioData::Const>, DestType> (numInterleavedChannels, 1);
            }

            using SourceType = AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const>;

            if (isLittleEndian)
                return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::LittleEndian, InterleavedType, AudioData::NonConst>> (1, numInterleavedChannels);

            return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::BigEndian, InterleavedType, AudioData::NonConst>> (1, numInterleavedChannels);
        }
    };

    static AudioData::Converter* createConverter (bool forInput, int bitDepth,
                                                  bool isFloat, bool isLittleEndian, bool useOnlyLower24Bits,
                                                  int numInterleavedChannels,
                                                  bool interleaved)
    {
        JUCE_ALSA_LOG ("format: bitDepth=" << bitDepth << ", isFloat=" << (int) isFloat
                        << ", isLittleEndian=" << (int) isLittleEndian << ", numChannels=" << numInterleavedChannels);

        if (isFloat)         return ConverterHelper <AudioData::Float32>::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved);
        if (bitDepth == 16)  return ConverterHelper <AudioData::Int16>  ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved);
        if (bitDepth == 24)  return ConverterHelper <AudioData::Int24>  ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved);

        jassert (bitDepth == 32);

        if (useOnlyLower24Bits)
            return ConverterHelper <AudioData::Int24in32>::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved);

        return ConverterHelper <AudioData::Int32>::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved);
    }

    //==============================================================================
    bool failed (const int errorNum)
    {
        if (errorNum >= 0)
            return false;

        error = snd_strerror (errorNum);
        JUCE_ALSA_LOG ("ALSA error: " << error);
        return true;
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSADevice)
};

//==============================================================================
class ALSAThread final : public Thread
{
public:
    ALSAThread (const String& inputDeviceID, const String& outputDeviceID)
        : Thread (SystemStats::getJUCEVersion() + ": ALSA"),
          inputId (inputDeviceID),
          outputId (outputDeviceID)
    {
        initialiseRatesAndChannels();
    }

    ~ALSAThread() override
    {
        close();
    }

    void open (BigInteger inputChannels,
               BigInteger outputChannels,
               double newSampleRate,
               int newBufferSize)
    {
        close();

        error.clear();
        sampleRate = newSampleRate;
        bufferSize = newBufferSize;

        int maxInputsRequested = inputChannels.getHighestBit() + 1;
        maxInputsRequested = jmax ((int) minChansIn, jmin ((int) maxChansIn, maxInputsRequested));

        inputChannelBuffer.setSize (maxInputsRequested, bufferSize);
        inputChannelBuffer.clear();
        inputChannelDataForCallback.clear();
        currentInputChans.clear();

        if (inputChannels.getHighestBit() >= 0)
        {
            for (int i = 0; i < maxInputsRequested; ++i)
            {
                if (inputChannels[i])
                {
                    inputChannelDataForCallback.add (inputChannelBuffer.getReadPointer (i));
                    currentInputChans.setBit (i);
                }
            }
        }

        ensureMinimumNumBitsSet (outputChannels, (int) minChansOut);

        int maxOutputsRequested = outputChannels.getHighestBit() + 1;
        maxOutputsRequested = jmax ((int) minChansOut, jmin ((int) maxChansOut, maxOutputsRequested));

        outputChannelBuffer.setSize (maxOutputsRequested, bufferSize);
        outputChannelBuffer.clear();
        outputChannelDataForCallback.clear();
        currentOutputChans.clear();

        // Note that the input device is opened before an output, because we've heard
        // of drivers where doing it in the reverse order mysteriously fails. If this
        // order also causes problems, let us know and we'll see if we can find a compromise!

        if (inputChannelDataForCallback.size() > 0 && inputId.isNotEmpty())
        {
            inputDevice.reset (new ALSADevice (inputId, true));

            if (inputDevice->error.isNotEmpty())
            {
                error = inputDevice->error;
                inputDevice.reset();
                return;
            }

            ensureMinimumNumBitsSet (currentInputChans, (int) minChansIn);

            if (! inputDevice->setParameters ((unsigned int) sampleRate,
                                              jlimit ((int) minChansIn, (int) maxChansIn, currentInputChans.getHighestBit() + 1),
                                              bufferSize))
            {
                error = inputDevice->error;
                inputDevice.reset();
                return;
            }

            inputLatency = inputDevice->latency;
        }

        if (outputChannels.getHighestBit() >= 0)
        {
            for (int i = 0; i < maxOutputsRequested; ++i)
            {
                if (outputChannels[i])
                {
                    outputChannelDataForCallback.add (outputChannelBuffer.getWritePointer (i));
                    currentOutputChans.setBit (i);
                }
            }
        }

        if (outputChannelDataForCallback.size() > 0 && outputId.isNotEmpty())
        {
            outputDevice.reset (new ALSADevice (outputId, false));

            if (outputDevice->error.isNotEmpty())
            {
                error = outputDevice->error;
                outputDevice.reset();
                return;
            }

            if (! outputDevice->setParameters ((unsigned int) sampleRate,
                                               jlimit ((int) minChansOut, (int) maxChansOut,
                                                       currentOutputChans.getHighestBit() + 1),
                                               bufferSize))
            {
                error = outputDevice->error;
                outputDevice.reset();
                return;
            }

            outputLatency = outputDevice->latency;
        }

        if (outputDevice == nullptr && inputDevice == nullptr)
        {
            error = "no channels";
            return;
        }

        if (outputDevice != nullptr && inputDevice != nullptr)
            snd_pcm_link (outputDevice->handle, inputDevice->handle);

        if (inputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (inputDevice->handle)))
            return;

        if (outputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (outputDevice->handle)))
            return;

        startThread (Priority::high);

        int count = 1000;

        while (numCallbacks == 0)
        {
            sleep (5);

            if (--count < 0 || ! isThreadRunning())
            {
                error = "device didn't start";
                break;
            }
        }
    }

    void close()
    {
        if (isThreadRunning())
        {
            // problem: when pulseaudio is suspended (with pasuspend) , the ALSAThread::run is just stuck in
            // snd_pcm_writei -- no error, no nothing it just stays stuck. So the only way I found to exit "nicely"
            // (that is without the "killing thread by force" of stopThread) , is to just call snd_pcm_close from
            // here which will cause the thread to resume, and exit
            signalThreadShouldExit();

            const int callbacksToStop = numCallbacks;

            if ((! waitForThreadToExit (400)) && audioIoInProgress && numCallbacks == callbacksToStop)
            {
                JUCE_ALSA_LOG ("Thread is stuck in i/o. Is pulseaudio suspended?");

                if (outputDevice != nullptr) outputDevice->closeNow();
                if (inputDevice != nullptr) inputDevice->closeNow();
            }
        }

        stopThread (6000);

        inputDevice.reset();
        outputDevice.reset();

        inputChannelBuffer.setSize (1, 1);
        outputChannelBuffer.setSize (1, 1);

        numCallbacks = 0;
    }

    void setCallback (AudioIODeviceCallback* const newCallback) noexcept
    {
        const ScopedLock sl (callbackLock);
        callback = newCallback;
    }

    void run() override
    {
        while (! threadShouldExit())
        {
            if (inputDevice != nullptr && inputDevice->handle != nullptr)
            {
                if (outputDevice == nullptr || outputDevice->handle == nullptr)
                {
                    JUCE_ALSA_FAILED (snd_pcm_wait (inputDevice->handle, 2000));

                    if (threadShouldExit())
                        break;

                    auto avail = snd_pcm_avail_update (inputDevice->handle);

                    if (avail < 0)
                        JUCE_ALSA_FAILED (snd_pcm_recover (inputDevice->handle, (int) avail, 0));
                }

                audioIoInProgress = true;

                if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize))
                {
                    JUCE_ALSA_LOG ("Read failure");
                    break;
                }

                audioIoInProgress = false;
            }

            if (threadShouldExit())
                break;

            {
                const ScopedLock sl (callbackLock);
                ++numCallbacks;

                if (callback != nullptr)
                {
                    callback->audioDeviceIOCallbackWithContext (inputChannelDataForCallback.getRawDataPointer(),
                                                                inputChannelDataForCallback.size(),
                                                                outputChannelDataForCallback.getRawDataPointer(),
                                                                outputChannelDataForCallback.size(),
                                                                bufferSize,
                                                                {});
                }
                else
                {
                    for (int i = 0; i < outputChannelDataForCallback.size(); ++i)
                        zeromem (outputChannelDataForCallback[i], (size_t) bufferSize * sizeof (float));
                }
            }

            if (outputDevice != nullptr && outputDevice->handle != nullptr)
            {
                JUCE_ALSA_FAILED (snd_pcm_wait (outputDevice->handle, 2000));

                if (threadShouldExit())
                    break;

                auto avail = snd_pcm_avail_update (outputDevice->handle);

                if (avail < 0)
                    JUCE_ALSA_FAILED (snd_pcm_recover (outputDevice->handle, (int) avail, 0));

                audioIoInProgress = true;

                if (! outputDevice->writeToOutputDevice (outputChannelBuffer, bufferSize))
                {
                    JUCE_ALSA_LOG ("write failure");
                    break;
                }

                audioIoInProgress = false;
            }
        }

        audioIoInProgress = false;
    }

    int getBitDepth() const noexcept
    {
        if (outputDevice != nullptr)
            return outputDevice->bitDepth;

        if (inputDevice != nullptr)
            return inputDevice->bitDepth;

        return 16;
    }

    int getXRunCount() const noexcept
    {
        int result = 0;

        if (outputDevice != nullptr)
            result += outputDevice->underrunCount;

        if (inputDevice != nullptr)
            result += inputDevice->overrunCount;

        return result;
    }

    //==============================================================================
    String error;
    double sampleRate = 0;
    int bufferSize = 0, outputLatency = 0, inputLatency = 0;
    BigInteger currentInputChans, currentOutputChans;

    Array<double> sampleRates;
    StringArray channelNamesOut, channelNamesIn;
    AudioIODeviceCallback* callback = nullptr;

private:
    //==============================================================================
    const String inputId, outputId;
    std::unique_ptr<ALSADevice> outputDevice, inputDevice;
    std::atomic<int> numCallbacks { 0 };
    std::atomic<bool> audioIoInProgress { false };

    CriticalSection callbackLock;

    AudioBuffer<float> inputChannelBuffer, outputChannelBuffer;
    Array<const float*> inputChannelDataForCallback;
    Array<float*> outputChannelDataForCallback;

    unsigned int minChansOut = 0, maxChansOut = 0;
    unsigned int minChansIn = 0, maxChansIn = 0;

    bool failed (const int errorNum)
    {
        if (errorNum >= 0)
            return false;

        error = snd_strerror (errorNum);
        JUCE_ALSA_LOG ("ALSA error: " << error);
        return true;
    }

    void initialiseRatesAndChannels()
    {
        sampleRates.clear();
        channelNamesOut.clear();
        channelNamesIn.clear();
        minChansOut = 0;
        maxChansOut = 0;
        minChansIn = 0;
        maxChansIn = 0;
        unsigned int dummy = 0;

        getDeviceProperties (inputId, dummy, dummy, minChansIn, maxChansIn, sampleRates, false, true);
        getDeviceProperties (outputId, minChansOut, maxChansOut, dummy, dummy, sampleRates, true, false);

        for (unsigned int i = 0; i < maxChansOut; ++i)
            channelNamesOut.add ("channel " + String ((int) i + 1));

        for (unsigned int i = 0; i < maxChansIn; ++i)
            channelNamesIn.add ("channel " + String ((int) i + 1));
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAThread)
};


//==============================================================================
class ALSAAudioIODevice final : public AudioIODevice
{
public:
    ALSAAudioIODevice (const String& deviceName,
                       const String& deviceTypeName,
                       const String& inputDeviceID,
                       const String& outputDeviceID)
        : AudioIODevice (deviceName, deviceTypeName),
          inputId (inputDeviceID),
          outputId (outputDeviceID),
          internal (inputDeviceID, outputDeviceID)
    {
    }

    ~ALSAAudioIODevice() override
    {
        close();
    }

    StringArray getOutputChannelNames() override            { return internal.channelNamesOut; }
    StringArray getInputChannelNames() override             { return internal.channelNamesIn; }

    Array<double> getAvailableSampleRates() override        { return internal.sampleRates; }

    Array<int> getAvailableBufferSizes() override
    {
        Array<int> r;
        int n = 16;

        for (int i = 0; i < 50; ++i)
        {
            r.add (n);
            n += n < 64 ? 16
                        : (n < 512 ? 32
                                   : (n < 1024 ? 64
                                               : (n < 2048 ? 128 : 256)));
        }

        return r;
    }

    int getDefaultBufferSize() override                      { return 512; }

    String open (const BigInteger& inputChannels,
                 const BigInteger& outputChannels,
                 double sampleRate,
                 int bufferSizeSamples) override
    {
        close();

        if (bufferSizeSamples <= 0)
            bufferSizeSamples = getDefaultBufferSize();

        if (sampleRate <= 0)
        {
            for (int i = 0; i < internal.sampleRates.size(); ++i)
            {
                double rate = internal.sampleRates[i];

                if (rate >= 44100)
                {
                    sampleRate = rate;
                    break;
                }
            }
        }

        internal.open (inputChannels, outputChannels,
                       sampleRate, bufferSizeSamples);

        isOpen_ = internal.error.isEmpty();
        return internal.error;
    }

    void close() override
    {
        stop();
        internal.close();
        isOpen_ = false;
    }

    bool isOpen() override                           { return isOpen_; }
    bool isPlaying() override                        { return isStarted && internal.error.isEmpty(); }
    String getLastError() override                   { return internal.error; }

    int getCurrentBufferSizeSamples() override       { return internal.bufferSize; }
    double getCurrentSampleRate() override           { return internal.sampleRate; }
    int getCurrentBitDepth() override                { return internal.getBitDepth(); }

    BigInteger getActiveOutputChannels() const override    { return internal.currentOutputChans; }
    BigInteger getActiveInputChannels() const override     { return internal.currentInputChans; }

    int getOutputLatencyInSamples() override         { return internal.outputLatency; }
    int getInputLatencyInSamples() override          { return internal.inputLatency; }

    int getXRunCount() const noexcept override       { return internal.getXRunCount(); }

    void start (AudioIODeviceCallback* callback) override
    {
        if (! isOpen_)
            callback = nullptr;

        if (callback != nullptr)
            callback->audioDeviceAboutToStart (this);

        internal.setCallback (callback);

        isStarted = (callback != nullptr);
    }

    void stop() override
    {
        auto oldCallback = internal.callback;

        start (nullptr);

        if (oldCallback != nullptr)
            oldCallback->audioDeviceStopped();
    }

    String inputId, outputId;

private:
    bool isOpen_ = false, isStarted = false;
    ALSAThread internal;
};


//==============================================================================
class ALSAAudioIODeviceType final : public AudioIODeviceType
{
public:
    ALSAAudioIODeviceType (bool onlySoundcards, const String& deviceTypeName)
        : AudioIODeviceType (deviceTypeName),
          listOnlySoundcards (onlySoundcards)
    {
       #if ! JUCE_ALSA_LOGGING
        snd_lib_error_set_handler (&silentErrorHandler);
       #endif
    }

    ~ALSAAudioIODeviceType() override
    {
       #if ! JUCE_ALSA_LOGGING
        snd_lib_error_set_handler (nullptr);
       #endif

        snd_config_update_free_global(); // prevent valgrind from screaming about alsa leaks
    }

    //==============================================================================
    void scanForDevices() override
    {
        if (hasScanned)
            return;

        hasScanned = true;
        inputNames.clear();
        inputIds.clear();
        outputNames.clear();
        outputIds.clear();

        JUCE_ALSA_LOG ("scanForDevices()");

        if (listOnlySoundcards)
            enumerateAlsaSoundcards();
        else
            enumerateAlsaPCMDevices();

        inputNames.appendNumbersToDuplicates (false, true);
        outputNames.appendNumbersToDuplicates (false, true);
    }

    StringArray getDeviceNames (bool wantInputNames) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        return wantInputNames ? inputNames : outputNames;
    }

    int getDefaultDeviceIndex (bool forInput) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        auto idx = (forInput ? inputIds : outputIds).indexOf ("default");
        return idx >= 0 ? idx : 0;
    }

    bool hasSeparateInputsAndOutputs() const override { return true; }

    int getIndexOfDevice (AudioIODevice* device, bool asInput) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        if (auto* d = dynamic_cast<ALSAAudioIODevice*> (device))
            return asInput ? inputIds.indexOf (d->inputId)
                           : outputIds.indexOf (d->outputId);

        return -1;
    }

    AudioIODevice* createDevice (const String& outputDeviceName,
                                 const String& inputDeviceName) override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        auto inputIndex = inputNames.indexOf (inputDeviceName);
        auto outputIndex = outputNames.indexOf (outputDeviceName);

        String deviceName (outputIndex >= 0 ? outputDeviceName
                                            : inputDeviceName);

        if (inputIndex >= 0 || outputIndex >= 0)
            return new ALSAAudioIODevice (deviceName, getTypeName(),
                                          inputIds [inputIndex],
                                          outputIds [outputIndex]);

        return nullptr;
    }

private:
    //==============================================================================
    StringArray inputNames, outputNames, inputIds, outputIds;
    bool hasScanned = false;
    const bool listOnlySoundcards;

    bool testDevice (const String& id, const String& outputName, const String& inputName)
    {
        unsigned int minChansOut = 0, maxChansOut = 0;
        unsigned int minChansIn = 0, maxChansIn = 0;
        Array<double> rates;

        bool isInput = inputName.isNotEmpty(), isOutput = outputName.isNotEmpty();
        getDeviceProperties (id, minChansOut, maxChansOut, minChansIn, maxChansIn, rates, isOutput, isInput);

        isInput  = maxChansIn > 0;
        isOutput = maxChansOut > 0;

        if ((isInput || isOutput) && rates.size() > 0)
        {
            JUCE_ALSA_LOG ("testDevice: '" << id.toUTF8().getAddress() << "' -> isInput: "
                            << (int) isInput << ", isOutput: " << (int) isOutput);

            if (isInput)
            {
                inputNames.add (inputName);
                inputIds.add (id);
            }

            if (isOutput)
            {
                outputNames.add (outputName);
                outputIds.add (id);
            }

            return isInput || isOutput;
        }

        return false;
    }

    void enumerateAlsaSoundcards()
    {
        snd_ctl_t* handle = nullptr;
        snd_ctl_card_info_t* info = nullptr;
        snd_ctl_card_info_alloca (&info);

        int cardNum = -1;

        while (outputIds.size() + inputIds.size() <= 64)
        {
            snd_card_next (&cardNum);

            if (cardNum < 0)
                break;

            if (JUCE_CHECKED_RESULT (snd_ctl_open (&handle, ("hw:" + String (cardNum)).toRawUTF8(), SND_CTL_NONBLOCK)) >= 0)
            {
                if (JUCE_CHECKED_RESULT (snd_ctl_card_info (handle, info)) >= 0)
                {
                    String cardId (snd_ctl_card_info_get_id (info));

                    if (cardId.removeCharacters ("0123456789").isEmpty())
                        cardId = String (cardNum);

                    String cardName = snd_ctl_card_info_get_name (info);

                    if (cardName.isEmpty())
                        cardName = cardId;

                    int device = -1;

                    snd_pcm_info_t* pcmInfo;
                    snd_pcm_info_alloca (&pcmInfo);

                    for (;;)
                    {
                        if (snd_ctl_pcm_next_device (handle, &device) < 0 || device < 0)
                            break;

                        snd_pcm_info_set_device (pcmInfo, (unsigned int) device);

                        for (unsigned int subDevice = 0, nbSubDevice = 1; subDevice < nbSubDevice; ++subDevice)
                        {
                            snd_pcm_info_set_subdevice (pcmInfo, subDevice);
                            snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_CAPTURE);
                            const bool isInput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0);

                            snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_PLAYBACK);
                            const bool isOutput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0);

                            if (! (isInput || isOutput))
                                continue;

                            if (nbSubDevice == 1)
                                nbSubDevice = snd_pcm_info_get_subdevices_count (pcmInfo);

                            String id, name;

                            if (nbSubDevice == 1)
                            {
                                id << "hw:" << cardId << "," << device;
                                name << cardName << ", " << snd_pcm_info_get_name (pcmInfo);
                            }
                            else
                            {
                                id << "hw:" << cardId << "," << device << "," << (int) subDevice;
                                name << cardName << ", " << snd_pcm_info_get_name (pcmInfo)
                                     << " {" <<  snd_pcm_info_get_subdevice_name (pcmInfo) << "}";
                            }

                            JUCE_ALSA_LOG ("Soundcard ID: " << id << ", name: '" << name
                                            << ", isInput:"  << (int) isInput
                                            << ", isOutput:" << (int) isOutput << "\n");

                            if (isInput)
                            {
                                inputNames.add (name);
                                inputIds.add (id);
                            }

                            if (isOutput)
                            {
                                outputNames.add (name);
                                outputIds.add (id);
                            }
                        }
                    }
                }

                JUCE_CHECKED_RESULT (snd_ctl_close (handle));
            }
        }
    }

    /* Enumerates all ALSA output devices (as output by the command aplay -L)
       Does not try to open the devices (with "testDevice" for example),
       so that it also finds devices that are busy and not yet available.
    */
    void enumerateAlsaPCMDevices()
    {
        void** hints = nullptr;

        if (JUCE_CHECKED_RESULT (snd_device_name_hint (-1, "pcm", &hints)) == 0)
        {
            for (char** h = (char**) hints; *h; ++h)
            {
                const String id (hintToString (*h, "NAME"));
                const String description (hintToString (*h, "DESC"));
                const String ioid (hintToString (*h, "IOID"));

                JUCE_ALSA_LOG ("ID: " << id << "; desc: " << description << "; ioid: " << ioid);

                String ss = id.fromFirstOccurrenceOf ("=", false, false)
                              .upToFirstOccurrenceOf (",", false, false);

                if (id.isEmpty()
                     || id.startsWith ("default:") || id.startsWith ("sysdefault:")
                     || id.startsWith ("plughw:") || id == "null")
                    continue;

                String name (description.replace ("\n", "; "));

                if (name.isEmpty())
                    name = id;

                bool isOutput = (ioid != "Input");
                bool isInput  = (ioid != "Output");

                // ALSA advertises dmix and dsnoop as input/output devices, but
                // opening dmix as input, or dsnoop as output will trigger errors
                isInput  = isInput  && ! id.startsWith ("dmix");
                isOutput = isOutput && ! id.startsWith ("dsnoop");

                if (isInput)
                {
                    inputNames.add (name);
                    inputIds.add (id);
                }

                if (isOutput)
                {
                    outputNames.add (name);
                    outputIds.add (id);
                }
            }

            snd_device_name_free_hint (hints);
        }

        // sometimes the "default" device is not listed, but it is nice to see it explicitly in the list
        if (! outputIds.contains ("default"))
            testDevice ("default", "Default ALSA Output", "Default ALSA Input");

        // same for the pulseaudio plugin
        if (! outputIds.contains ("pulse"))
            testDevice ("pulse", "Pulseaudio output", "Pulseaudio input");

        // make sure the default device is listed first, and followed by the pulse device (if present)
        auto idx = outputIds.indexOf ("pulse");
        outputIds.move (idx, 0);
        outputNames.move (idx, 0);

        idx = inputIds.indexOf ("pulse");
        inputIds.move (idx, 0);
        inputNames.move (idx, 0);

        idx = outputIds.indexOf ("default");
        outputIds.move (idx, 0);
        outputNames.move (idx, 0);

        idx = inputIds.indexOf ("default");
        inputIds.move (idx, 0);
        inputNames.move (idx, 0);
    }

    static String hintToString (const void* hints, const char* type)
    {
        char* hint = snd_device_name_get_hint (hints, type);
        auto s = String::fromUTF8 (hint);
        ::free (hint);
        return s;
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAAudioIODeviceType)
};

}

//==============================================================================
static inline AudioIODeviceType* createAudioIODeviceType_ALSA_Soundcards()
{
    return new ALSAAudioIODeviceType (true, "ALSA HW");
}

static inline AudioIODeviceType* createAudioIODeviceType_ALSA_PCMDevices()
{
    return new ALSAAudioIODeviceType (false, "ALSA");
}

} // namespace juce
