/*
 * Carla RtAudio Engine
 * Copyright (C) 2012-2013 Filipe Coelho <falktx@falktx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or any later version.
 *
 * This program 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.
 *
 * For a full copy of the GNU General Public License see the GPL.txt file
 */

#ifdef WANT_RTAUDIO

#include "CarlaEngineInternal.hpp"
#include "CarlaBackendUtils.hpp"
#include "CarlaMIDI.h"
#include "RtList.hpp"

#include "RtAudio.h"
#include "RtMidi.h"

CARLA_BACKEND_START_NAMESPACE

#if 0
} // Fix editor indentation
#endif

// -------------------------------------------------------------------------------------------------------------------

RtMidi::Api getMatchedAudioMidiAPi(const RtAudio::Api rtApi)
{
    switch (rtApi)
    {
    case RtAudio::UNSPECIFIED:
        return RtMidi::UNSPECIFIED;

    case RtAudio::LINUX_ALSA:
    case RtAudio::LINUX_OSS:
    case RtAudio::LINUX_PULSE:
        return RtMidi::LINUX_ALSA;

    case RtAudio::UNIX_JACK:
#if defined(CARLA_OS_WIN)
        return RtMidi::WINDOWS_MM;
#elif defined(CARLA_OS_MAC)
        return RtMidi::MACOSX_CORE;
#elif defined(CARLA_OS_LINUX)
        return RtMidi::LINUX_ALSA;
#else
        return RtMidi::UNIX_JACK;
#endif

    case RtAudio::MACOSX_CORE:
        return RtMidi::MACOSX_CORE;

    case RtAudio::WINDOWS_ASIO:
    case RtAudio::WINDOWS_DS:
        return RtMidi::WINDOWS_MM;

    case RtAudio::RTAUDIO_DUMMY:
        return RtMidi::RTMIDI_DUMMY;
    }

    return RtMidi::UNSPECIFIED;
}

// -------------------------------------------------------------------------------------------------------------------
// RtAudio Engine

class CarlaEngineRtAudio : public CarlaEngine
{
public:
    CarlaEngineRtAudio(const RtAudio::Api api)
        : CarlaEngine(),
          fAudio(api),
          fAudioIsInterleaved(false),
          fAudioIsReady(false),
          fAudioInBuf1(nullptr),
          fAudioInBuf2(nullptr),
          fAudioOutBuf1(nullptr),
          fAudioOutBuf2(nullptr),
          fMidiIn(getMatchedAudioMidiAPi(api), "Carla"),
          fMidiOut(getMatchedAudioMidiAPi(api), "Carla")
    {
        carla_debug("CarlaEngineRtAudio::CarlaEngineRtAudio(%i)", api);

        // just to make sure
        fOptions.forceStereo = true;
        fOptions.processMode = PROCESS_MODE_CONTINUOUS_RACK;
    }

    ~CarlaEngineRtAudio()
    {
        carla_debug("CarlaEngineRtAudio::~CarlaEngineRtAudio()");
        CARLA_ASSERT(fAudioInBuf1 == nullptr);
        CARLA_ASSERT(fAudioInBuf2 == nullptr);
        CARLA_ASSERT(fAudioOutBuf1 == nullptr);
        CARLA_ASSERT(fAudioOutBuf2 == nullptr);
    }

    // -------------------------------------

    bool init(const char* const clientName)
    {
        carla_debug("CarlaEngineRtAudio::init(\"%s\")", clientName);
        CARLA_ASSERT(! fAudioIsReady);
        CARLA_ASSERT(fAudioInBuf1 == nullptr);
        CARLA_ASSERT(fAudioInBuf2 == nullptr);
        CARLA_ASSERT(fAudioOutBuf1 == nullptr);
        CARLA_ASSERT(fAudioOutBuf2 == nullptr);
        CARLA_ASSERT(clientName != nullptr);

        if (fAudio.getDeviceCount() == 0)
        {
            setLastError("No audio devices available for this driver");
            return false;
        }

        fBufferSize = fOptions.preferredBufferSize;

        // Audio
        {
            RtAudio::StreamParameters iParams, oParams;
            iParams.deviceId = fAudio.getDefaultInputDevice();
            oParams.deviceId = fAudio.getDefaultOutputDevice();
            iParams.nChannels = 2;
            oParams.nChannels = 2;

            RtAudio::StreamOptions rtOptions;
            rtOptions.flags = RTAUDIO_MINIMIZE_LATENCY | RTAUDIO_HOG_DEVICE | RTAUDIO_SCHEDULE_REALTIME;
            rtOptions.numberOfBuffers = 2;
            rtOptions.streamName = clientName;
            rtOptions.priority = 85;

            if (fAudio.getCurrentApi() != RtAudio::LINUX_PULSE)
            {
                rtOptions.flags |= RTAUDIO_NONINTERLEAVED;
                fAudioIsInterleaved = false;

                if (fAudio.getCurrentApi() == RtAudio::LINUX_ALSA)
                    rtOptions.flags |= RTAUDIO_ALSA_USE_DEFAULT;
            }
            else
                fAudioIsInterleaved = true;

            try {
                fAudio.openStream(&oParams, &iParams, RTAUDIO_FLOAT32, fOptions.preferredSampleRate, &fBufferSize, carla_rtaudio_process_callback, this, &rtOptions);
            }
            catch (RtError& e)
            {
                carla_stderr2("RtAudio::openStream() failed");
                if (e.getType() == RtError::SYSTEM_ERROR)
                    setLastError("Stream cannot be opened with the specified parameters");
                else if (e.getType() == RtError::INVALID_USE)
                    setLastError("Invalid device ID");
                else
                    setLastError("Unknown error");
                return false;
            }

            try {
                fAudio.startStream();
            }
            catch (RtError& e)
            {
                carla_stderr2("RtAudio::startStream() failed");
                setLastError(e.what());
                fAudio.closeStream();
                return false;
            }

            fAudioInBuf1  = new float[fBufferSize];
            fAudioInBuf2  = new float[fBufferSize];
            fAudioOutBuf1 = new float[fBufferSize];
            fAudioOutBuf2 = new float[fBufferSize];

            fSampleRate = fAudio.getStreamSampleRate();
        }

        // MIDI
        {
            fMidiIn.setCallback(carla_rtmidi_callback, this);
            fMidiIn.openVirtualPort("events-in");
            fMidiOut.openVirtualPort("events-out");
        }

        fAudioIsReady = true;

        return CarlaEngine::init(clientName);
    }

    bool close()
    {
        carla_debug("CarlaEngineRtAudio::close()");
        CARLA_ASSERT(fAudioIsReady);
        CARLA_ASSERT(fAudioInBuf1 != nullptr);
        CARLA_ASSERT(fAudioInBuf2 != nullptr);
        CARLA_ASSERT(fAudioOutBuf1 != nullptr);
        CARLA_ASSERT(fAudioOutBuf2 != nullptr);

        CarlaEngine::close();

        fAudioIsReady = false;

        if (fAudio.isStreamRunning())
        {
            try {
                fAudio.stopStream();
            }
            catch (...) {}
        }

        if (fAudio.isStreamOpen())
        {
            try {
                fAudio.closeStream();
            }
            catch (...) {}
        }

        fMidiIn.cancelCallback();
        fMidiIn.closePort();
        fMidiOut.closePort();

        if (fAudioInBuf1 != nullptr)
        {
            delete[] fAudioInBuf1;
            fAudioInBuf1 = nullptr;
        }

        if (fAudioInBuf2 != nullptr)
        {
            delete[] fAudioInBuf2;
            fAudioInBuf2 = nullptr;
        }

        if (fAudioOutBuf1 != nullptr)
        {
            delete[] fAudioOutBuf1;
            fAudioOutBuf1 = nullptr;
        }

        if (fAudioOutBuf2 != nullptr)
        {
            delete[] fAudioOutBuf2;
            fAudioOutBuf2 = nullptr;
        }

        fMidiInEvents.clear();
        fMidiOutEvents.clear();

        return true;
    }

    bool isRunning() const
    {
        return fAudio.isStreamRunning();
    }

    bool isOffline() const
    {
        return false;
    }

    EngineType type() const
    {
        return kEngineTypeRtAudio;
    }

    // -------------------------------------

protected:
    void handleAudioProcessCallback(void* outputBuffer, void* inputBuffer, unsigned int nframes, double streamTime, RtAudioStreamStatus status)
    {
        // get buffers from RtAudio
        float* insPtr  = (float*)inputBuffer;
        float* outsPtr = (float*)outputBuffer;

        // assert buffers
        CARLA_ASSERT(insPtr != nullptr);
        CARLA_ASSERT(outsPtr != nullptr);

        if (! fAudioIsReady)
        {
            carla_zeroFloat(outsPtr, nframes*2);
            return proccessPendingEvents();
        }

        if (kData->curPluginCount == 0)
        {
            // pass-through
            if (fOptions.processMode == PROCESS_MODE_CONTINUOUS_RACK)
                carla_copyFloat(outsPtr, insPtr, nframes*2);

            return proccessPendingEvents();
        }

        // initialize audio input
        if (fAudioIsInterleaved)
        {
            for (unsigned int i=0; i < nframes*2; i++)
            {
                if (i % 2 == 0)
                    fAudioInBuf1[i/2] = insPtr[i];
                else
                    fAudioInBuf2[i/2] = insPtr[i];
            }
        }
        else
        {
            carla_copyFloat(fAudioInBuf1, insPtr, nframes);
            carla_copyFloat(fAudioInBuf2, insPtr+nframes, nframes);
        }

        // initialize audio output
        carla_zeroFloat(fAudioOutBuf1, fBufferSize);
        carla_zeroFloat(fAudioOutBuf2, fBufferSize);

        // initialize input events
        carla_zeroMem(kData->rack.in, sizeof(EngineEvent)*RACK_EVENT_COUNT);

        if (fMidiInEvents.mutex.tryLock())
        {
            uint32_t engineEventIndex = 0;
            fMidiInEvents.splice();

            while (! fMidiInEvents.data.isEmpty())
            {
                const RtMidiEvent& midiEvent = fMidiInEvents.data.getFirst(true);

                EngineEvent* const engineEvent = &kData->rack.in[engineEventIndex++];
                engineEvent->clear();

                const uint8_t midiStatus  = MIDI_GET_STATUS_FROM_DATA(midiEvent.data);
                const uint8_t midiChannel = MIDI_GET_CHANNEL_FROM_DATA(midiEvent.data);

                engineEvent->channel = midiChannel;

                if (midiEvent.time < fTimeInfo.frame)
                    engineEvent->time = 0;
                else if (midiEvent.time >= fTimeInfo.frame + nframes)
                {
                    engineEvent->time = fTimeInfo.frame + nframes-1;
                    carla_stderr("MIDI Event in the future!, %i vs %i", engineEvent->time, fTimeInfo.frame);
                }
                else
                    engineEvent->time = midiEvent.time - fTimeInfo.frame;

                //carla_stdout("Got midi, time %f vs %i", midiEvent.time, engineEvent->time);

                if (MIDI_IS_STATUS_CONTROL_CHANGE(midiStatus))
                {
                    const uint8_t midiControl = midiEvent.data[1];
                    engineEvent->type         = kEngineEventTypeControl;

                    if (MIDI_IS_CONTROL_BANK_SELECT(midiControl))
                    {
                        const uint8_t midiBank  = midiEvent.data[2];

                        engineEvent->ctrl.type  = kEngineControlEventTypeMidiBank;
                        engineEvent->ctrl.param = midiBank;
                        engineEvent->ctrl.value = 0.0;
                    }
                    else if (midiControl == MIDI_CONTROL_ALL_SOUND_OFF)
                    {
                        engineEvent->ctrl.type  = kEngineControlEventTypeAllSoundOff;
                        engineEvent->ctrl.param = 0;
                        engineEvent->ctrl.value = 0.0;
                    }
                    else if (midiControl == MIDI_CONTROL_ALL_NOTES_OFF)
                    {
                        engineEvent->ctrl.type  = kEngineControlEventTypeAllNotesOff;
                        engineEvent->ctrl.param = 0;
                        engineEvent->ctrl.value = 0.0;
                    }
                    else
                    {
                        const uint8_t midiValue = midiEvent.data[2];

                        engineEvent->ctrl.type  = kEngineControlEventTypeParameter;
                        engineEvent->ctrl.param = midiControl;
                        engineEvent->ctrl.value = double(midiValue)/127.0;
                    }
                }
                else if (MIDI_IS_STATUS_PROGRAM_CHANGE(midiStatus))
                {
                    const uint8_t midiProgram = midiEvent.data[1];
                    engineEvent->type         = kEngineEventTypeControl;

                    engineEvent->ctrl.type  = kEngineControlEventTypeMidiProgram;
                    engineEvent->ctrl.param = midiProgram;
                    engineEvent->ctrl.value = 0.0;
                }
                else
                {
                    engineEvent->type = kEngineEventTypeMidi;

                    engineEvent->midi.data[0] = midiStatus;
                    engineEvent->midi.data[1] = midiEvent.data[1];
                    engineEvent->midi.data[2] = midiEvent.data[2];
                    engineEvent->midi.size    = midiEvent.size;
                }

                if (engineEventIndex >= RACK_EVENT_COUNT)
                    break;
            }

            fMidiInEvents.mutex.unlock();
        }

        // create audio buffers
        float* inBuf[2]  = { fAudioInBuf1, fAudioInBuf2 };
        float* outBuf[2] = { fAudioOutBuf1, fAudioOutBuf2 };

        processRack(inBuf, outBuf, nframes);

        // output audio
        if (fAudioIsInterleaved)
        {
            for (unsigned int i=0; i < nframes*2; i++)
            {
                if (i % 2 == 0)
                    outsPtr[i] = fAudioOutBuf1[i/2];
                else
                    outsPtr[i] = fAudioOutBuf2[i/2];
            }
        }
        else
        {
            carla_copyFloat(outsPtr, fAudioOutBuf1, nframes);
            carla_copyFloat(outsPtr+nframes, fAudioOutBuf2, nframes);
        }

        // output events
        {
            // TODO
            //fMidiOut.sendMessage();
        }

        proccessPendingEvents();

        return;

        // unused
        (void)streamTime;
        (void)status;
    }

    void handleMidiCallback(double timeStamp, std::vector<unsigned char>* const message)
    {
        const size_t messageSize = message->size();
        static uint32_t lastTime = 0;

        if (messageSize == 0 || messageSize > 3)
            return;

        timeStamp /= 2;

        if (timeStamp > 0.95)
            timeStamp = 0.95;

        RtMidiEvent midiEvent;
        midiEvent.time = fTimeInfo.frame + (timeStamp*(double)fBufferSize);
        carla_stdout("Put midi, frame:%09i/%09i, stamp:%g", fTimeInfo.frame, midiEvent.time, timeStamp);

        if (midiEvent.time < lastTime)
            midiEvent.time = lastTime;
        else
            lastTime = midiEvent.time;

        if (messageSize == 1)
        {
            midiEvent.data[0] = message->at(0);
            midiEvent.data[1] = 0;
            midiEvent.data[2] = 0;
            midiEvent.size    = 1;
        }
        else if (messageSize == 2)
        {
            midiEvent.data[0] = message->at(0);
            midiEvent.data[1] = message->at(1);
            midiEvent.data[2] = 0;
            midiEvent.size    = 2;
        }
        else
        {
            midiEvent.data[0] = message->at(0);
            midiEvent.data[1] = message->at(1);
            midiEvent.data[2] = message->at(2);
            midiEvent.size    = 3;
        }

        fMidiInEvents.append(midiEvent);
    }

    // -------------------------------------

private:
    RtAudio fAudio;
    bool    fAudioIsInterleaved;
    bool    fAudioIsReady;
    float*  fAudioInBuf1;
    float*  fAudioInBuf2;
    float*  fAudioOutBuf1;
    float*  fAudioOutBuf2;

    RtMidiIn  fMidiIn;
    RtMidiOut fMidiOut;

    struct RtMidiEvent {
        uint32_t time;
        unsigned char data[3];
        unsigned char size;
    };

    struct RtMidiEvents {
        CarlaMutex mutex;
        RtList<RtMidiEvent>::Pool dataPool;
        RtList<RtMidiEvent> data;
        RtList<RtMidiEvent> dataPending;

        RtMidiEvents()
            : dataPool(512, 512),
              data(&dataPool),
              dataPending(&dataPool) {}

        ~RtMidiEvents()
        {
            clear();
        }

        void append(const RtMidiEvent& event)
        {
            mutex.lock();
            dataPending.append(event);
            mutex.unlock();
        }

        void clear()
        {
            mutex.lock();
            data.clear();
            dataPending.clear();
            mutex.unlock();
        }

        void splice()
        {
            dataPending.spliceAppend(data, true);
        }
    };

    RtMidiEvents fMidiInEvents;
    RtMidiEvents fMidiOutEvents;

    #define handlePtr ((CarlaEngineRtAudio*)userData)

    static int carla_rtaudio_process_callback(void* outputBuffer, void* inputBuffer, unsigned int nframes, double streamTime, RtAudioStreamStatus status, void* userData)
    {
        handlePtr->handleAudioProcessCallback(outputBuffer, inputBuffer, nframes, streamTime, status);
        return 0;
    }

    static void carla_rtmidi_callback(double timeStamp, std::vector<unsigned char>* message, void* userData)
    {
        handlePtr->handleMidiCallback(timeStamp, message);
    }

    #undef handlePtr

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CarlaEngineRtAudio)
};

// -----------------------------------------

static std::vector<RtAudio::Api> sRtAudioApis;

static void initRtApis()
{
    static bool initiated = false;

    if (! initiated)
    {
        initiated = true;

        RtAudio::getCompiledApi(sRtAudioApis);
    }
}

CarlaEngine* CarlaEngine::newRtAudio(RtAudioApi api)
{
    RtAudio::Api rtApi = RtAudio::UNSPECIFIED;

    switch (api)
    {
    case RTAUDIO_DUMMY:
        rtApi = RtAudio::RTAUDIO_DUMMY;
        break;
    case RTAUDIO_LINUX_ALSA:
        rtApi = RtAudio::LINUX_ALSA;
        break;
    case RTAUDIO_LINUX_PULSE:
        rtApi = RtAudio::LINUX_PULSE;
        break;
    case RTAUDIO_LINUX_OSS:
        rtApi = RtAudio::LINUX_OSS;
        break;
    case RTAUDIO_UNIX_JACK:
        rtApi = RtAudio::UNIX_JACK;
        break;
    case RTAUDIO_MACOSX_CORE:
        rtApi = RtAudio::MACOSX_CORE;
        break;
    case RTAUDIO_WINDOWS_ASIO:
        rtApi = RtAudio::WINDOWS_ASIO;
        break;
    case RTAUDIO_WINDOWS_DS:
        rtApi = RtAudio::WINDOWS_DS;
        break;
    }

    return new CarlaEngineRtAudio(rtApi);
}

size_t CarlaEngine::getRtAudioApiCount()
{
    initRtApis();

    return sRtAudioApis.size();
}

const char* CarlaEngine::getRtAudioApiName(const unsigned int index)
{
    initRtApis();

    if (index < sRtAudioApis.size())
    {
        const RtAudio::Api& api(sRtAudioApis[index]);

        switch (api)
        {
        case RtAudio::UNSPECIFIED:
            return "Unspecified";
        case RtAudio::LINUX_ALSA:
            return "ALSA";
        case RtAudio::LINUX_PULSE:
            return "PulseAudio";
        case RtAudio::LINUX_OSS:
            return "OSS";
        case RtAudio::UNIX_JACK:
#if defined(CARLA_OS_WIN)
        return "JACK with WinMM";
#elif defined(CARLA_OS_MAC)
        return "JACK with CoreMidi";
#elif defined(CARLA_OS_LINUX)
        return "JACK with ALSA-MIDI";
#else
        return "JACK (RtAudio)";
#endif
        case RtAudio::MACOSX_CORE:
            return "CoreAudio";
        case RtAudio::WINDOWS_ASIO:
            return "ASIO";
        case RtAudio::WINDOWS_DS:
            return "DirectSound";
        case RtAudio::RTAUDIO_DUMMY:
            return "Dummy";
        }
    }

    return nullptr;
}

// -----------------------------------------

CARLA_BACKEND_END_NAMESPACE

#endif // CARLA_ENGINE_RTAUDIO
