/*
 * Carla Plugin Host
 * Copyright (C) 2011-2022 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 doc/GPL.txt file.
 */

#include "CarlaEngineGraph.hpp"
#include "CarlaEngineInit.hpp"
#include "CarlaEngineInternal.hpp"
#include "CarlaStringList.hpp"
#include "CarlaBackendUtils.hpp"

#include <SDL.h>

#ifndef HAVE_SDL2
typedef Uint32 SDL_AudioDeviceID;
#endif

#ifndef SDL_HINT_AUDIO_DEVICE_APP_NAME
# define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
#endif

#ifndef SDL_HINT_AUDIO_DEVICE_STREAM_NAME
# define SDL_HINT_AUDIO_DEVICE_STREAM_NAME "SDL_AUDIO_DEVICE_STREAM_NAME"
#endif

CARLA_BACKEND_START_NAMESPACE

// -------------------------------------------------------------------------------------------------------------------
// Global static data

static CarlaStringList gDeviceNames;

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

static void initAudioDevicesIfNeeded()
{
    static bool needsInit = true;

    if (! needsInit)
        return;

    needsInit = false;

#ifdef HAVE_SDL2
    SDL_InitSubSystem(SDL_INIT_AUDIO);

    const int numDevices = SDL_GetNumAudioDevices(0);

    for (int i=0; i<numDevices; ++i)
        gDeviceNames.append(SDL_GetAudioDeviceName(i, 0));
#else
    SDL_Init(SDL_INIT_AUDIO);
#endif
}

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

class CarlaEngineSDL : public CarlaEngine
{
public:
    CarlaEngineSDL()
        : CarlaEngine(),
          fDeviceId(0),
          fDeviceName(),
          fAudioOutCount(0),
          fAudioIntBufOut(nullptr)
    {
        carla_debug("CarlaEngineSDL::CarlaEngineSDL()");

        // just to make sure
        pData->options.transportMode = ENGINE_TRANSPORT_MODE_INTERNAL;
    }

    ~CarlaEngineSDL() override
    {
        CARLA_SAFE_ASSERT(fAudioOutCount == 0);
        carla_debug("CarlaEngineSDL::~CarlaEngineSDL()");
    }

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

    bool init(const char* const clientName) override
    {
        CARLA_SAFE_ASSERT_RETURN(fDeviceId == 0, false);
        CARLA_SAFE_ASSERT_RETURN(fAudioOutCount == 0, false);
        CARLA_SAFE_ASSERT_RETURN(clientName != nullptr && clientName[0] != '\0', false);
        carla_debug("CarlaEngineSDL::init(\"%s\")", clientName);

        if (pData->options.processMode != ENGINE_PROCESS_MODE_CONTINUOUS_RACK && pData->options.processMode != ENGINE_PROCESS_MODE_PATCHBAY)
        {
            setLastError("Invalid process mode");
            return false;
        }

        SDL_AudioSpec requested, received;
        carla_zeroStruct(requested);
#ifdef HAVE_SDL2
        requested.format = AUDIO_F32SYS;
#else
        requested.format = AUDIO_S16SYS;
#endif
        requested.channels = 2;
        requested.freq = static_cast<int>(pData->options.audioSampleRate);
        requested.samples = static_cast<Uint16>(pData->options.audioBufferSize);
        requested.callback = carla_sdl_process_callback;
        requested.userdata = this;

#ifdef HAVE_SDL2
        SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, clientName);
        // SDL_SetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME, );
        SDL_SetHint(SDL_HINT_AUDIO_RESAMPLING_MODE, "2");

        const char* const deviceName = pData->options.audioDevice != nullptr && pData->options.audioDevice[0] != '\0'
                                     ? pData->options.audioDevice
                                     : nullptr;

        int flags = SDL_AUDIO_ALLOW_FREQUENCY_CHANGE;
        if (pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY)
            flags |= SDL_AUDIO_ALLOW_CHANNELS_CHANGE;

        fDeviceId = SDL_OpenAudioDevice(deviceName, 0, &requested, &received, flags);
#else
        fDeviceId = SDL_OpenAudio(&requested, &received) == 0 ? 1 : 0;
#endif

        if (fDeviceId == 0)
        {
            setLastError(SDL_GetError());
            return false;
        }

        if (received.channels == 0)
        {
#ifdef HAVE_SDL2
            SDL_CloseAudioDevice(fDeviceId);
#else
            SDL_CloseAudio();
#endif
            fDeviceId = 0;
            setLastError("No output channels available");
            return false;
        }

        if (! pData->init(clientName))
        {
            close();
            setLastError("Failed to init internal data");
            return false;
        }

        pData->bufferSize = received.samples;
        pData->sampleRate = received.freq;
        pData->initTime(pData->options.transportExtra);

        fAudioOutCount = received.channels;
        fAudioIntBufOut = new float*[fAudioOutCount];
        for (uint i=0; i<fAudioOutCount; ++i)
            fAudioIntBufOut[i] = new float[received.samples];

        pData->graph.create(0, fAudioOutCount, 0, 0);

#ifdef HAVE_SDL2
        SDL_PauseAudioDevice(fDeviceId, 0);
#else
        SDL_PauseAudio(0);
#endif
        carla_stdout("open fAudioOutCount %d %d %d | %d vs %d",
                     fAudioOutCount, received.samples, received.freq,
                     received.format, requested.format);

        patchbayRefresh(true, false, false);

        if (pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY)
            refreshExternalGraphPorts<PatchbayGraph>(pData->graph.getPatchbayGraph(), false, false);

        callback(true, true,
                 ENGINE_CALLBACK_ENGINE_STARTED,
                 0,
                 pData->options.processMode,
                 pData->options.transportMode,
                 static_cast<int>(pData->bufferSize),
                 static_cast<float>(pData->sampleRate),
                 getCurrentDriverName());
        return true;
    }

    bool close() override
    {
        carla_debug("CarlaEngineSDL::close()");

        // close device
        if (fDeviceId != 0)
        {
            // SDL_PauseAudioDevice(fDeviceId, 1);
#ifdef HAVE_SDL2
            SDL_CloseAudioDevice(fDeviceId);
#else
            SDL_CloseAudio();
#endif
            fDeviceId = 0;
        }

        // clear engine data
        CarlaEngine::close();

        pData->graph.destroy();

        // cleanup
        if (fAudioIntBufOut != nullptr)
        {
            for (uint i=0; i<fAudioOutCount; ++i)
                delete[] fAudioIntBufOut[i];
            delete[] fAudioIntBufOut;
            fAudioIntBufOut = nullptr;
        }

        fAudioOutCount = 0;
        fDeviceName.clear();

        return false;
    }

    bool isRunning() const noexcept override
    {
        return fDeviceId != 0 /*&& SDL_GetAudioDeviceStatus(fDeviceId) == SDL_AUDIO_PLAYING*/;
    }

    bool isOffline() const noexcept override
    {
        return false;
    }

    EngineType getType() const noexcept override
    {
        return kEngineTypeSDL;
    }

    const char* getCurrentDriverName() const noexcept override
    {
        return "SDL";
    }

    // -------------------------------------------------------------------
    // Patchbay

    template<class Graph>
    bool refreshExternalGraphPorts(Graph* const graph, const bool sendHost, const bool sendOSC)
    {
        CARLA_SAFE_ASSERT_RETURN(graph != nullptr, false);

        char strBuf[STR_MAX+1U];
        strBuf[STR_MAX] = '\0';

        ExternalGraph& extGraph(graph->extGraph);

        // ---------------------------------------------------------------
        // clear last ports

        extGraph.clear();

        // ---------------------------------------------------------------
        // fill in new ones

        // Audio Out
        for (uint i=0; i < fAudioOutCount; ++i)
        {
            std::snprintf(strBuf, STR_MAX, "playback_%i", i+1);

            PortNameToId portNameToId;
            portNameToId.setData(kExternalGraphGroupAudioOut, i+1, strBuf, "");

            extGraph.audioPorts.outs.append(portNameToId);
        }

        // ---------------------------------------------------------------
        // now refresh

        if (sendHost || sendOSC)
            graph->refresh(sendHost, sendOSC, true, fDeviceName.buffer());

        return true;
    }

    bool patchbayRefresh(const bool sendHost, const bool sendOSC, const bool external) override
    {
        CARLA_SAFE_ASSERT_RETURN(pData->graph.isReady(), false);

        if (pData->options.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK)
            return refreshExternalGraphPorts<RackGraph>(pData->graph.getRackGraph(), sendHost, sendOSC);

        if (sendHost)
            pData->graph.setUsingExternalHost(external);
        if (sendOSC)
            pData->graph.setUsingExternalOSC(external);

        if (external)
            return refreshExternalGraphPorts<PatchbayGraph>(pData->graph.getPatchbayGraph(), sendHost, sendOSC);

        return CarlaEngine::patchbayRefresh(sendHost, sendOSC, false);
    }

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

protected:
    void handleAudioProcessCallback(uchar* const stream, const int len)
    {
        // safety checks
        CARLA_SAFE_ASSERT_RETURN(stream != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(len > 0,);

#ifdef HAVE_SDL2
        // direct float type
        float* const fstream = (float*)stream;
        const uint ulen = static_cast<uint>(static_cast<uint>(len) / sizeof(float) / fAudioOutCount);
#else
        // signed 16bit int
        int16_t* const istream = (int16_t*)stream;
        const uint ulen = static_cast<uint>(static_cast<uint>(len) / sizeof(int16_t) / fAudioOutCount);
#endif

        const PendingRtEventsRunner prt(this, ulen, true);

        // init our deinterleaved audio buffers
        for (uint i=0, count=fAudioOutCount; i<count; ++i)
            carla_zeroFloats(fAudioIntBufOut[i], ulen);

        // initialize events
        carla_zeroStructs(pData->events.in,  kMaxEngineEventInternalCount);
        carla_zeroStructs(pData->events.out, kMaxEngineEventInternalCount);

        pData->graph.process(pData, nullptr, fAudioIntBufOut, ulen);

        // interleave audio back
        for (uint i=0; i < fAudioOutCount; ++i)
        {
            for (uint j=0; j < ulen; ++j)
            {
#ifdef HAVE_SDL2
                // direct float type
                fstream[j * fAudioOutCount + i] = fAudioIntBufOut[i][j];
#else
                // signed 16bit int
                istream[j * fAudioOutCount + i] = lrintf(carla_fixedValue(-1.0f, 1.0f, fAudioIntBufOut[i][j]) * 32767.0f);
#endif
            }
        }
    }

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

    bool connectExternalGraphPort(const uint connectionType, const uint portId, const char* const portName) override
    {
        CARLA_SAFE_ASSERT_RETURN(connectionType != 0 || (portName != nullptr && portName[0] != '\0'), false);
        carla_debug("CarlaEngineSDL::connectExternalGraphPort(%u, %u, \"%s\")", connectionType, portId, portName);

        switch (connectionType)
        {
        case kExternalGraphConnectionAudioIn1:
        case kExternalGraphConnectionAudioIn2:
        case kExternalGraphConnectionAudioOut1:
        case kExternalGraphConnectionAudioOut2:
            return CarlaEngine::connectExternalGraphPort(connectionType, portId, portName);

        case kExternalGraphConnectionMidiInput:
        case kExternalGraphConnectionMidiOutput:
            break;
        }

        return false;
    }

    bool disconnectExternalGraphPort(const uint connectionType, const uint portId, const char* const portName) override
    {
        CARLA_SAFE_ASSERT_RETURN(connectionType != 0 || (portName != nullptr && portName[0] != '\0'), false);
        carla_debug("CarlaEngineSDL::disconnectExternalGraphPort(%u, %u, \"%s\")", connectionType, portId, portName);

        switch (connectionType)
        {
        case kExternalGraphConnectionAudioIn1:
        case kExternalGraphConnectionAudioIn2:
        case kExternalGraphConnectionAudioOut1:
        case kExternalGraphConnectionAudioOut2:
            return CarlaEngine::disconnectExternalGraphPort(connectionType, portId, portName);

        case kExternalGraphConnectionMidiInput:
        case kExternalGraphConnectionMidiOutput:
            break;
        }

        return false;
    }

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

private:
    SDL_AudioDeviceID fDeviceId;

    // current device name
    CarlaString fDeviceName;

    // deinterleaved buffers
    uint fAudioOutCount;
    float** fAudioIntBufOut;

    #define handlePtr ((CarlaEngineSDL*)userData)

    static void carla_sdl_process_callback(void* userData, uchar* stream, int len)
    {
        handlePtr->handleAudioProcessCallback(stream, len);
    }

    #undef handlePtr

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CarlaEngineSDL)
};

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

namespace EngineInit {

CarlaEngine* newSDL()
{
    initAudioDevicesIfNeeded();
    return new CarlaEngineSDL();
}

const char* const* getSDLDeviceNames()
{
    initAudioDevicesIfNeeded();

    if (gDeviceNames.count() == 0)
    {
        static const char* deviceNames[] = { "Default", nullptr };
        return deviceNames;
    }

    static const CharStringListPtr deviceNames = gDeviceNames.toCharStringListPtr();
    return deviceNames;
}

}

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

CARLA_BACKEND_END_NAMESPACE
