/*
 * Carla Plugin Host
 * Copyright (C) 2011-2019 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
 */

#include "CarlaEngineGraph.hpp"
#include "CarlaEngineInternal.hpp"

#include <ctime>
#include <sys/time.h>

CARLA_BACKEND_START_NAMESPACE

// -------------------------------------------------------------------------------------------------------------------
// Dummy Engine

class CarlaEngineDummy : public CarlaEngine,
                         public CarlaThread
{
public:
    CarlaEngineDummy()
        : CarlaEngine(),
          CarlaThread("CarlaEngineDummy"),
          fRunning(false)
    {
        carla_debug("CarlaEngineDummy::CarlaEngineDummy()");

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

    ~CarlaEngineDummy() override
    {
        carla_debug("CarlaEngineDummy::~CarlaEngineDummy()");
    }

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

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

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

        fRunning = true;

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

        pData->bufferSize = pData->options.audioBufferSize;
        pData->sampleRate = pData->options.audioSampleRate;
        pData->initTime(pData->options.transportExtra);

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

        if (! startThread(true))
        {
            close();
            setLastError("Failed to start dummy audio thread");
            return false;
        }

        patchbayRefresh(true, 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("CarlaEngineDummy::close()");

        fRunning = false;
        stopThread(-1);
        CarlaEngine::close();

        pData->graph.destroy();
        return true;
    }

    bool isRunning() const noexcept override
    {
        return fRunning;
    }

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

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

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

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

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

        RackGraph* const graph = pData->graph.getRackGraph();
        CARLA_SAFE_ASSERT_RETURN(graph != nullptr, false);

        ExternalGraph& extGraph(graph->extGraph);

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

        extGraph.clear();

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

        {
            PortNameToId portNameToId;
            portNameToId.setData(kExternalGraphGroupAudioIn, 1, "capture_1", "");

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

        {
            PortNameToId portNameToId;
            portNameToId.setData(kExternalGraphGroupAudioIn, 2, "capture_2", "");

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

        {
            PortNameToId portNameToId;
            portNameToId.setData(kExternalGraphGroupAudioOut, 1, "playback_1", "");

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

        {
            PortNameToId portNameToId;
            portNameToId.setData(kExternalGraphGroupAudioOut, 2, "playback_2", "");

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

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

        if (sendHost || sendOSC)
            graph->refresh(sendHost, sendOSC, false, "Dummy");

        return true;
    }

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

protected:
    static int64_t getTimeInMicroseconds() noexcept
    {
    #if defined(CARLA_OS_MAC) || defined(CARLA_OS_WIN)
        struct timeval tv;
        gettimeofday(&tv, nullptr);

        return (tv.tv_sec * 1000000) + tv.tv_usec;
    #else
        struct timespec ts;
    # ifdef CLOCK_MONOTONIC_RAW
        clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
    # else
        clock_gettime(CLOCK_MONOTONIC, &ts);
    # endif

        return (ts.tv_sec * 1000000) + (ts.tv_nsec / 1000);
    #endif
    }

    void run() override
    {
        const uint32_t bufferSize = pData->bufferSize;
        const int64_t cycleTime = static_cast<int64_t>(
            static_cast<double>(bufferSize) / pData->sampleRate * 1000000 + 0.5);

        carla_stdout("CarlaEngineDummy audio thread started, cycle time: " P_INT64 "ms", cycleTime / 1000);

        float* audioIns[2] = {
            (float*)std::malloc(sizeof(float)*bufferSize),
            (float*)std::malloc(sizeof(float)*bufferSize),
        };
        CARLA_SAFE_ASSERT_RETURN(audioIns[0] != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(audioIns[1] != nullptr,);

        float* audioOuts[2] = {
            (float*)std::malloc(sizeof(float)*bufferSize),
            (float*)std::malloc(sizeof(float)*bufferSize),
        };
        CARLA_SAFE_ASSERT_RETURN(audioOuts[0] != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(audioOuts[1] != nullptr,);

        carla_zeroFloats(audioIns[0], bufferSize);
        carla_zeroFloats(audioIns[1], bufferSize);
        carla_zeroStructs(pData->events.in,  kMaxEngineEventInternalCount);

        while (! shouldThreadExit())
        {
            const int64_t oldTime = getTimeInMicroseconds();

            const PendingRtEventsRunner prt(this, bufferSize, true);

            carla_zeroFloats(audioOuts[0], bufferSize);
            carla_zeroFloats(audioOuts[1], bufferSize);
            carla_zeroStructs(pData->events.out, kMaxEngineEventInternalCount);

            pData->graph.process(pData, audioIns, audioOuts, bufferSize);

            const int64_t newTime = getTimeInMicroseconds();
            CARLA_SAFE_ASSERT_CONTINUE(newTime >= oldTime);

            const int64_t remainingTime = cycleTime - (newTime - oldTime);

            if (remainingTime <= 0)
            {
                ++pData->xruns;
                carla_stdout("XRUN! remaining time: " P_INT64 ", old: " P_INT64 ", new: " P_INT64 ")",
                             remainingTime, oldTime, newTime);
            }
            else
            {
                CARLA_SAFE_ASSERT_CONTINUE(remainingTime < 1000000); // 1 sec
                carla_msleep(static_cast<uint>(remainingTime / 1000));
            }
        }

        std::free(audioIns[0]);
        std::free(audioIns[1]);
        std::free(audioOuts[0]);
        std::free(audioOuts[1]);

        carla_stdout("CarlaEngineDummy audio thread finished");
    }

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

private:
    bool fRunning;

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CarlaEngineDummy)
};

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

CarlaEngine* CarlaEngine::newDummy()
{
    return new CarlaEngineDummy();
}

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

CARLA_BACKEND_END_NAMESPACE
