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

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

   Permission is granted to use this software under the terms of either:
   a) the GPL v2 (or any later version)
   b) the Affero GPL v3

   Details of these licenses can be found at: www.gnu.org/licenses

   JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

   ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.juce.com for more information.

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

const char* const openSLTypeName = "Android OpenSL";

bool isOpenSLAvailable()
{
    DynamicLibrary library;
    return library.open ("libOpenSLES.so");
}

//==============================================================================
class OpenSLAudioIODevice  : public AudioIODevice,
                             public Thread
{
public:
    OpenSLAudioIODevice (const String& deviceName)
        : AudioIODevice (deviceName, openSLTypeName),
          Thread ("OpenSL"),
          callback (nullptr), sampleRate (0), deviceOpen (false),
          inputBuffer (2, 2), outputBuffer (2, 2)
    {
        // OpenSL has piss-poor support for determining latency, so the only way I can find to
        // get a number for this is by asking the AudioTrack/AudioRecord classes..
        AndroidAudioIODevice javaDevice (String::empty);

        // this is a total guess about how to calculate the latency, but seems to vaguely agree
        // with the devices I've tested.. YMMV
        inputLatency  = (javaDevice.minBufferSizeIn  * 2) / 3;
        outputLatency = (javaDevice.minBufferSizeOut * 2) / 3;

        const int64 longestLatency = jmax (inputLatency, outputLatency);
        const int64 totalLatency = inputLatency + outputLatency;
        inputLatency  = (int) ((longestLatency * inputLatency)  / totalLatency) & ~15;
        outputLatency = (int) ((longestLatency * outputLatency) / totalLatency) & ~15;
    }

    ~OpenSLAudioIODevice()
    {
        close();
    }

    bool openedOk() const       { return engine.outputMixObject != nullptr; }

    StringArray getOutputChannelNames() override
    {
        StringArray s;
        s.add ("Left");
        s.add ("Right");
        return s;
    }

    StringArray getInputChannelNames() override
    {
        StringArray s;
        s.add ("Audio Input");
        return s;
    }

    Array<double> getAvailableSampleRates() override
    {
        static const double rates[] = { 8000.0, 16000.0, 32000.0, 44100.0, 48000.0 };
        return Array<double> (rates, numElementsInArray (rates));
    }

    Array<int> getAvailableBufferSizes() override
    {
        static const int sizes[] = { 256, 512, 768, 1024, 1280, 1600 }; // must all be multiples of the block size
        return Array<int> (sizes, numElementsInArray (sizes));
    }

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

        lastError.clear();
        sampleRate = (int) requestedSampleRate;

        int preferredBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize;

        activeOutputChans = outputChannels;
        activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false);
        numOutputChannels = activeOutputChans.countNumberOfSetBits();

        activeInputChans = inputChannels;
        activeInputChans.setRange (1, activeInputChans.getHighestBit(), false);
        numInputChannels = activeInputChans.countNumberOfSetBits();

        actualBufferSize = preferredBufferSize;

        inputBuffer.setSize  (jmax (1, numInputChannels),  actualBufferSize);
        outputBuffer.setSize (jmax (1, numOutputChannels), actualBufferSize);
        outputBuffer.clear();

        recorder = engine.createRecorder (numInputChannels,  sampleRate);
        player   = engine.createPlayer   (numOutputChannels, sampleRate);

        startThread (8);

        deviceOpen = true;
        return lastError;
    }

    void close() override
    {
        stop();
        stopThread (6000);
        deviceOpen = false;
        recorder = nullptr;
        player = nullptr;
    }

    int getDefaultBufferSize() override                 { return 1024; }
    int getOutputLatencyInSamples() override            { return outputLatency; }
    int getInputLatencyInSamples() override             { return inputLatency; }
    bool isOpen() override                              { return deviceOpen; }
    int getCurrentBufferSizeSamples() override          { return actualBufferSize; }
    int getCurrentBitDepth() override                   { return 16; }
    double getCurrentSampleRate() override              { return sampleRate; }
    BigInteger getActiveOutputChannels() const override { return activeOutputChans; }
    BigInteger getActiveInputChannels() const override  { return activeInputChans; }
    String getLastError() override                      { return lastError; }
    bool isPlaying() override                           { return callback != nullptr; }

    void start (AudioIODeviceCallback* newCallback) override
    {
        stop();

        if (deviceOpen && callback != newCallback)
        {
            if (newCallback != nullptr)
                newCallback->audioDeviceAboutToStart (this);

            setCallback (newCallback);
        }
    }

    void stop() override
    {
        if (AudioIODeviceCallback* const oldCallback = setCallback (nullptr))
            oldCallback->audioDeviceStopped();
    }

    bool setAudioPreprocessingEnabled (bool enable) override
    {
        return recorder != nullptr && recorder->setAudioPreprocessingEnabled (enable);
    }

private:
    //==================================================================================================
    CriticalSection callbackLock;
    AudioIODeviceCallback* callback;
    int actualBufferSize, sampleRate;
    int inputLatency, outputLatency;
    bool deviceOpen;
    String lastError;
    BigInteger activeOutputChans, activeInputChans;
    int numInputChannels, numOutputChannels;
    AudioSampleBuffer inputBuffer, outputBuffer;
    struct Player;
    struct Recorder;

    AudioIODeviceCallback* setCallback (AudioIODeviceCallback* const newCallback)
    {
        const ScopedLock sl (callbackLock);
        AudioIODeviceCallback* const oldCallback = callback;
        callback = newCallback;
        return oldCallback;
    }

    void run() override
    {
        if (recorder != nullptr)    recorder->start();
        if (player != nullptr)      player->start();

        while (! threadShouldExit())
        {
            if (player != nullptr)      player->writeBuffer (outputBuffer, *this);
            if (recorder != nullptr)    recorder->readNextBlock (inputBuffer, *this);

            const ScopedLock sl (callbackLock);

            if (callback != nullptr)
            {
                callback->audioDeviceIOCallback (numInputChannels  > 0 ? inputBuffer.getArrayOfReadPointers()   : nullptr, numInputChannels,
                                                 numOutputChannels > 0 ? outputBuffer.getArrayOfWritePointers() : nullptr, numOutputChannels,
                                                 actualBufferSize);
            }
            else
            {
                outputBuffer.clear();
            }
        }
    }

    //==================================================================================================
    struct Engine
    {
        Engine()
            : engineObject (nullptr), engineInterface (nullptr), outputMixObject (nullptr)
        {
            if (library.open ("libOpenSLES.so"))
            {
                typedef SLresult (*CreateEngineFunc) (SLObjectItf*, SLuint32, const SLEngineOption*, SLuint32, const SLInterfaceID*, const SLboolean*);

                if (CreateEngineFunc createEngine = (CreateEngineFunc) library.getFunction ("slCreateEngine"))
                {
                    check (createEngine (&engineObject, 0, nullptr, 0, nullptr, nullptr));

                    SLInterfaceID* SL_IID_ENGINE    = (SLInterfaceID*) library.getFunction ("SL_IID_ENGINE");
                    SL_IID_ANDROIDSIMPLEBUFFERQUEUE = (SLInterfaceID*) library.getFunction ("SL_IID_ANDROIDSIMPLEBUFFERQUEUE");
                    SL_IID_PLAY                     = (SLInterfaceID*) library.getFunction ("SL_IID_PLAY");
                    SL_IID_RECORD                   = (SLInterfaceID*) library.getFunction ("SL_IID_RECORD");
                    SL_IID_ANDROIDCONFIGURATION     = (SLInterfaceID*) library.getFunction ("SL_IID_ANDROIDCONFIGURATION");

                    check ((*engineObject)->Realize (engineObject, SL_BOOLEAN_FALSE));
                    check ((*engineObject)->GetInterface (engineObject, *SL_IID_ENGINE, &engineInterface));

                    check ((*engineInterface)->CreateOutputMix (engineInterface, &outputMixObject, 0, nullptr, nullptr));
                    check ((*outputMixObject)->Realize (outputMixObject, SL_BOOLEAN_FALSE));
                }
            }
        }

        ~Engine()
        {
            if (outputMixObject != nullptr) (*outputMixObject)->Destroy (outputMixObject);
            if (engineObject != nullptr)    (*engineObject)->Destroy (engineObject);
        }

        Player* createPlayer (const int numChannels, const int sampleRate)
        {
            if (numChannels <= 0)
                return nullptr;

            ScopedPointer<Player> player (new Player (numChannels, sampleRate, *this));
            return player->openedOk() ? player.release() : nullptr;
        }

        Recorder* createRecorder (const int numChannels, const int sampleRate)
        {
            if (numChannels <= 0)
                return nullptr;

            ScopedPointer<Recorder> recorder (new Recorder (numChannels, sampleRate, *this));
            return recorder->openedOk() ? recorder.release() : nullptr;
        }

        SLObjectItf engineObject;
        SLEngineItf engineInterface;
        SLObjectItf outputMixObject;

        SLInterfaceID* SL_IID_ANDROIDSIMPLEBUFFERQUEUE;
        SLInterfaceID* SL_IID_PLAY;
        SLInterfaceID* SL_IID_RECORD;
        SLInterfaceID* SL_IID_ANDROIDCONFIGURATION;

    private:
        DynamicLibrary library;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Engine)
    };

    //==================================================================================================
    struct BufferList
    {
        BufferList (const int numChannels_)
            : numChannels (numChannels_), bufferSpace (numChannels_ * numSamples * numBuffers), nextBlock (0)
        {
        }

        int16* waitForFreeBuffer (Thread& threadToCheck)
        {
            while (numBlocksOut.get() == numBuffers)
            {
                dataArrived.wait (1);

                if (threadToCheck.threadShouldExit())
                    return nullptr;
            }

            return getNextBuffer();
        }

        int16* getNextBuffer()
        {
            if (++nextBlock == numBuffers)
                nextBlock = 0;

            return bufferSpace + nextBlock * numChannels * numSamples;
        }

        void bufferReturned()           { --numBlocksOut; dataArrived.signal(); }
        void bufferSent()               { ++numBlocksOut; dataArrived.signal(); }

        int getBufferSizeBytes() const  { return numChannels * numSamples * sizeof (int16); }

        const int numChannels;
        enum { numSamples = 256, numBuffers = 16 };

    private:
        HeapBlock<int16> bufferSpace;
        int nextBlock;
        Atomic<int> numBlocksOut;
        WaitableEvent dataArrived;
    };

    //==================================================================================================
    struct Player
    {
        Player (int numChannels, int sampleRate, Engine& engine)
            : playerObject (nullptr), playerPlay (nullptr), playerBufferQueue (nullptr),
              bufferList (numChannels)
        {
            jassert (numChannels == 2);

            SLDataFormat_PCM pcmFormat =
            {
                SL_DATAFORMAT_PCM,
                (SLuint32) numChannels,
                (SLuint32) (sampleRate * 1000), // (sample rate units are millihertz)
                SL_PCMSAMPLEFORMAT_FIXED_16,
                SL_PCMSAMPLEFORMAT_FIXED_16,
                SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
                SL_BYTEORDER_LITTLEENDIAN
            };

            SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, bufferList.numBuffers };
            SLDataSource audioSrc = { &bufferQueue, &pcmFormat };

            SLDataLocator_OutputMix outputMix = { SL_DATALOCATOR_OUTPUTMIX, engine.outputMixObject };
            SLDataSink audioSink = { &outputMix, nullptr };

            // (SL_IID_BUFFERQUEUE is not guaranteed to remain future-proof, so use SL_IID_ANDROIDSIMPLEBUFFERQUEUE)
            const SLInterfaceID interfaceIDs[] = { *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
            const SLboolean flags[] = { SL_BOOLEAN_TRUE };

            check ((*engine.engineInterface)->CreateAudioPlayer (engine.engineInterface, &playerObject, &audioSrc, &audioSink,
                                                                 1, interfaceIDs, flags));

            check ((*playerObject)->Realize (playerObject, SL_BOOLEAN_FALSE));
            check ((*playerObject)->GetInterface (playerObject, *engine.SL_IID_PLAY, &playerPlay));
            check ((*playerObject)->GetInterface (playerObject, *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &playerBufferQueue));
            check ((*playerBufferQueue)->RegisterCallback (playerBufferQueue, staticCallback, this));
        }

        ~Player()
        {
            if (playerPlay != nullptr)
                check ((*playerPlay)->SetPlayState (playerPlay, SL_PLAYSTATE_STOPPED));

            if (playerBufferQueue != nullptr)
                check ((*playerBufferQueue)->Clear (playerBufferQueue));

            if (playerObject != nullptr)
                (*playerObject)->Destroy (playerObject);
        }

        bool openedOk() const noexcept      { return playerBufferQueue != nullptr; }

        void start()
        {
            jassert (openedOk());
            check ((*playerPlay)->SetPlayState (playerPlay, SL_PLAYSTATE_PLAYING));
        }

        void writeBuffer (const AudioSampleBuffer& buffer, Thread& thread)
        {
            jassert (buffer.getNumChannels() == bufferList.numChannels);
            jassert (buffer.getNumSamples() < bufferList.numSamples * bufferList.numBuffers);

            int offset = 0;
            int numSamples = buffer.getNumSamples();

            while (numSamples > 0)
            {
                int16* const destBuffer = bufferList.waitForFreeBuffer (thread);

                if (destBuffer == nullptr)
                    break;

                for (int i = 0; i < bufferList.numChannels; ++i)
                {
                    typedef AudioData::Pointer <AudioData::Int16,   AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> DstSampleType;
                    typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SrcSampleType;

                    DstSampleType dstData (destBuffer + i, bufferList.numChannels);
                    SrcSampleType srcData (buffer.getReadPointer (i, offset));
                    dstData.convertSamples (srcData, bufferList.numSamples);
                }

                check ((*playerBufferQueue)->Enqueue (playerBufferQueue, destBuffer, bufferList.getBufferSizeBytes()));
                bufferList.bufferSent();

                numSamples -= bufferList.numSamples;
                offset += bufferList.numSamples;
            }
        }

    private:
        SLObjectItf playerObject;
        SLPlayItf playerPlay;
        SLAndroidSimpleBufferQueueItf playerBufferQueue;

        BufferList bufferList;

        static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context)
        {
            jassert (queue == static_cast <Player*> (context)->playerBufferQueue); (void) queue;
            static_cast <Player*> (context)->bufferList.bufferReturned();
        }

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Player)
    };

    //==================================================================================================
    struct Recorder
    {
        Recorder (int numChannels, int sampleRate, Engine& engine)
            : recorderObject (nullptr), recorderRecord (nullptr),
              recorderBufferQueue (nullptr), configObject (nullptr),
              bufferList (numChannels)
        {
            jassert (numChannels == 1); // STEREO doesn't always work!!

            SLDataFormat_PCM pcmFormat =
            {
                SL_DATAFORMAT_PCM,
                (SLuint32) numChannels,
                (SLuint32) (sampleRate * 1000), // (sample rate units are millihertz)
                SL_PCMSAMPLEFORMAT_FIXED_16,
                SL_PCMSAMPLEFORMAT_FIXED_16,
                (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT),
                SL_BYTEORDER_LITTLEENDIAN
            };

            SLDataLocator_IODevice ioDevice = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr };
            SLDataSource audioSrc = { &ioDevice, nullptr };

            SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, bufferList.numBuffers };
            SLDataSink audioSink = { &bufferQueue, &pcmFormat };

            const SLInterfaceID interfaceIDs[] = { *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
            const SLboolean flags[] = { SL_BOOLEAN_TRUE };

            if (check ((*engine.engineInterface)->CreateAudioRecorder (engine.engineInterface, &recorderObject, &audioSrc,
                                                                       &audioSink, 1, interfaceIDs, flags)))
            {
                if (check ((*recorderObject)->Realize (recorderObject, SL_BOOLEAN_FALSE)))
                {
                    check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_RECORD, &recorderRecord));
                    check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue));
                    check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDCONFIGURATION, &configObject));
                    check ((*recorderBufferQueue)->RegisterCallback (recorderBufferQueue, staticCallback, this));
                    check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_STOPPED));

                    for (int i = bufferList.numBuffers; --i >= 0;)
                    {
                        int16* const buffer = bufferList.getNextBuffer();
                        jassert (buffer != nullptr);
                        enqueueBuffer (buffer);
                    }
                }
            }
        }

        ~Recorder()
        {
            if (recorderRecord != nullptr)
                check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_STOPPED));

            if (recorderBufferQueue != nullptr)
                check ((*recorderBufferQueue)->Clear (recorderBufferQueue));

            if (recorderObject != nullptr)
                (*recorderObject)->Destroy (recorderObject);
        }

        bool openedOk() const noexcept      { return recorderBufferQueue != nullptr; }

        void start()
        {
            jassert (openedOk());
            check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_RECORDING));
        }

        void readNextBlock (AudioSampleBuffer& buffer, Thread& thread)
        {
            jassert (buffer.getNumChannels() == bufferList.numChannels);
            jassert (buffer.getNumSamples() < bufferList.numSamples * bufferList.numBuffers);
            jassert ((buffer.getNumSamples() % bufferList.numSamples) == 0);

            int offset = 0;
            int numSamples = buffer.getNumSamples();

            while (numSamples > 0)
            {
                int16* const srcBuffer = bufferList.waitForFreeBuffer (thread);

                if (srcBuffer == nullptr)
                    break;

                for (int i = 0; i < bufferList.numChannels; ++i)
                {
                    typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DstSampleType;
                    typedef AudioData::Pointer <AudioData::Int16,   AudioData::LittleEndian, AudioData::Interleaved, AudioData::Const> SrcSampleType;

                    DstSampleType dstData (buffer.getWritePointer (i, offset));
                    SrcSampleType srcData (srcBuffer + i, bufferList.numChannels);
                    dstData.convertSamples (srcData, bufferList.numSamples);
                }

                enqueueBuffer (srcBuffer);

                numSamples -= bufferList.numSamples;
                offset += bufferList.numSamples;
            }
        }

        bool setAudioPreprocessingEnabled (bool enable)
        {
            SLuint32 mode = enable ? SL_ANDROID_RECORDING_PRESET_GENERIC
                                   : SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION;

            return configObject != nullptr
                     && check ((*configObject)->SetConfiguration (configObject, SL_ANDROID_KEY_RECORDING_PRESET, &mode, sizeof (mode)));
        }

    private:
        SLObjectItf recorderObject;
        SLRecordItf recorderRecord;
        SLAndroidSimpleBufferQueueItf recorderBufferQueue;
        SLAndroidConfigurationItf configObject;

        BufferList bufferList;

        void enqueueBuffer (int16* buffer)
        {
            check ((*recorderBufferQueue)->Enqueue (recorderBufferQueue, buffer, bufferList.getBufferSizeBytes()));
            bufferList.bufferSent();
        }

        static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context)
        {
            jassert (queue == static_cast <Recorder*> (context)->recorderBufferQueue); (void) queue;
            static_cast <Recorder*> (context)->bufferList.bufferReturned();
        }

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Recorder)
    };


    //==============================================================================
    Engine engine;

    ScopedPointer<Player> player;
    ScopedPointer<Recorder> recorder;

    //==============================================================================
    static bool check (const SLresult result)
    {
        jassert (result == SL_RESULT_SUCCESS);
        return result == SL_RESULT_SUCCESS;
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioIODevice)
};


//==============================================================================
class OpenSLAudioDeviceType  : public AudioIODeviceType
{
public:
    OpenSLAudioDeviceType()  : AudioIODeviceType (openSLTypeName) {}

    //==============================================================================
    void scanForDevices() {}
    StringArray getDeviceNames (bool wantInputNames) const              { return StringArray (openSLTypeName); }
    int getDefaultDeviceIndex (bool forInput) const                     { return 0; }
    int getIndexOfDevice (AudioIODevice* device, bool asInput) const    { return device != nullptr ? 0 : -1; }
    bool hasSeparateInputsAndOutputs() const                            { return false; }

    AudioIODevice* createDevice (const String& outputDeviceName,
                                 const String& inputDeviceName)
    {
        ScopedPointer<OpenSLAudioIODevice> dev;

        if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty())
        {
            dev = new OpenSLAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName
                                                                         : inputDeviceName);
            if (! dev->openedOk())
                dev = nullptr;
        }

        return dev.release();
    }

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioDeviceType)
};


//==============================================================================
AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_OpenSLES()
{
    return isOpenSLAvailable() ? new OpenSLAudioDeviceType() : nullptr;
}
