/*
    This file is part of Element
    Copyright (C) 2019  Kushview, LLC.  All rights reserved.

    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
    (at your option) 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.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "engine/AudioEngine.h"
#include "engine/GraphProcessor.h"
#include "engine/InternalFormat.h"
#include "engine/MidiClock.h"
#include "engine/MidiChannelMap.h"
#include "engine/MidiEngine.h"
#include "engine/MidiTranspose.h"
#include "engine/Transport.h"
#include "Globals.h"
#include "Settings.h"

namespace Element {

RootGraph::RootGraph() { }

void RootGraph::setPlayConfigFor (DeviceManager& devices)
{
    if (auto* device = devices.getCurrentAudioDevice())
        setPlayConfigFor (device);
    DeviceManager::AudioSettings setup;
    devices.getAudioDeviceSetup (setup);
    audioInName     = setup.inputDeviceName;
    audioOutName    = setup.outputDeviceName;
}

void RootGraph::setPlayConfigFor (AudioIODevice *device)
{
    jassert (device);
    
    const int numIns        = device->getActiveInputChannels().countNumberOfSetBits();
    const int numOuts       = device->getActiveOutputChannels().countNumberOfSetBits();
    const int bufferSize    = device->getCurrentBufferSizeSamples();
    const double sampleRate = device->getCurrentSampleRate();
    setPlayConfigDetails (numIns, numOuts, sampleRate, bufferSize);
    updateChannelNames (device);
    graphName = device->getName();
    if (graphName.isEmpty())
        graphName = "Device";
}

void RootGraph::setPlayConfigFor (const DeviceManager::AudioDeviceSetup& setup)
{
    setPlayConfigDetails (setup.inputChannels.countNumberOfSetBits(),
                          setup.outputChannels.countNumberOfSetBits(),
                          setup.sampleRate, setup.bufferSize);
}

const String RootGraph::getName() const { return graphName; }
    
const String RootGraph::getInputChannelName (int c) const { return audioInputNames[c]; }
    
const String RootGraph::getOutputChannelName (int c) const { return audioOutputNames[c]; }

void RootGraph::updateChannelNames (AudioIODevice* device)
{
    auto activeIn  = device->getActiveInputChannels();
    auto namesIn   = device->getInputChannelNames();
    auto activeOut = device->getActiveOutputChannels();
    auto namesOut  = device->getOutputChannelNames();
    audioOutputNames.clear();
    audioInputNames.clear();
    for (int i = 0; i < namesIn.size(); ++i)
        if (activeIn[i] == true)
            audioInputNames.add(namesIn[i]);
    for (int i = 0; i < namesOut.size(); ++i)
        if (activeOut[i] == true)
            audioOutputNames.add(namesOut[i]);
}

struct RootGraphRender : public AsyncUpdater
{
    std::function<void()> onActiveGraphChanged;

    RootGraphRender()
    {
        graphs.ensureStorageAllocated (32);
    }

    void handleAsyncUpdate() override
    {
        if (onActiveGraphChanged)
            onActiveGraphChanged();
    }

    const int setCurrentGraph (const int index)
    {
        if (index == currentGraph)
            return currentGraph;
        currentGraph = index;
        triggerAsyncUpdate();
        return currentGraph;
    }

    const int getCurrentGraphIndex() const { return currentGraph; }

    RootGraph* getCurrentGraph() const
    { 
        return isPositiveAndBelow (currentGraph, graphs.size()) ? graphs.getUnchecked(currentGraph) 
                                                                : nullptr; 
    }

    void prepareBuffers (const int numIns, const int numOuts, const int numSamples)
    {
        numInputChans   = numIns;
        numOutputChans  = numOuts;
        audioTemp.setSize (jmax (numIns, numOuts), numSamples);
        audioOut.setSize (audioTemp.getNumChannels(), audioTemp.getNumSamples());
    }

    void releaseBuffers()
    {
        numInputChans = numOutputChans = 0;
        midiOut.clear();
        midiTemp.clear();
        audioTemp.setSize (1, 1);
        audioOut.setSize (1, 1);
    }
    void dumpGraphs() {
        
    }

    void renderGraphs (AudioSampleBuffer& buffer, MidiBuffer& midi)
    {
       #if defined (EL_PRO)
        if (program.wasRequested())
        {
            if (! locked)
            {
                const int nextGraph = findGraphForProgram (program);
                if (nextGraph != currentGraph)
                {
                    setCurrentGraph (nextGraph);
                }
            }
            else
            {
                DBG("[EL] program change not handled: product locked");
            }

            program.reset();
        }
       #endif

        auto* const current  = getCurrentGraph();
        auto* const last     = (lastGraph >= 0 && lastGraph < graphs.size()) ? getGraph(lastGraph) : nullptr;
        
        if (current == nullptr || last == nullptr)
        {
            buffer.clear();
            midi.clear();
            return;
        }

        const int numSamples = buffer.getNumSamples();
        const int numChans   = buffer.getNumChannels();
        const bool graphChanged = lastGraph != currentGraph;
        const bool shouldProcess = true;
        const RootGraph::RenderMode mode = current->getRenderMode();
        const bool modeChanged = graphChanged && mode != last->getRenderMode();

        if (shouldProcess)
        {
			audioOut.setSize (buffer.getNumChannels(), buffer.getNumSamples(),
							  false, false, true);
			audioTemp.setSize (buffer.getNumChannels(), buffer.getNumSamples(),
							  false, false, true);

            // clear the mixing area
            for (int i = numChans; --i >= 0;)
                audioOut.clear (i, 0, numSamples);
            midiOut.clear();
            
            for (auto* const graph : graphs)
            {
                // copy inputs, clear outs if more than input count
                for (int i = 0; i < numInputChans; ++i)
                    audioTemp.copyFrom (i, 0, buffer, i, 0, numSamples);
                for (int i = numInputChans; i < numChans; ++i)
                    audioTemp.clear (i, 0, numSamples);
                
                // clear so messages: avoids feedback loop when IO node ins are 
                // connected to IO node outs
                midiTemp.clear (0, numSamples);
                
                if ((last == graph && graphChanged && last->isSingle())
                    || (graphChanged && current != nullptr && current->isSingle() && graph != current))
                {
                    // send kill messages to the last graph(s) when the graph changes
                    // see http://nickfever.com/music/midi-cc-list
                    for (int i = 0; i < 16; ++i)
                    {
                        // sustain pedal off
                        midiTemp.addEvent (MidiMessage::controllerEvent (i + 1, 64, 0), 0);
                        // Sostenuto off
                        midiTemp.addEvent (MidiMessage::controllerEvent (i + 1, 66, 0), 0);
                        // Hold off
                        midiTemp.addEvent (MidiMessage::controllerEvent (i + 1, 69, 0), 0);

                        midiTemp.addEvent (MidiMessage::allNotesOff (i + 1), 0);
                    }
                }
                else if ((current == graph && graph->isSingle()) 
                            || (current != nullptr && !current->isSingle() && !graph->isSingle()))
                {
                    // current single graph or parallel graphs get MIDI always
                    midiTemp.addEvents (midi, 0, numSamples, 0);
                }

                {
                    const ScopedLock sl (graph->getCallbackLock());
                    if (graph->isSuspended())
                    {
                        graph->processBlockBypassed (audioTemp, midiTemp);
                    }
                    else
                    {
                        graph->processBlock (audioTemp, midiTemp);
                    }
                }
                
                if (graphChanged && ((current->isSingle() && current != graph) ||
                                     (modeChanged && !current->isSingle() && graph->isSingle())))
                                     
                {
                    // DBG("  FADE OUT LAST GRAPH: " << graph->engineIndex);
                    for (int i = 0; i < numOutputChans; ++i)
                            audioOut.addFromWithRamp (i, 0, audioTemp.getReadPointer (i), 
                                                      numSamples, 1.f, 0.f);
                }
                else if ((graph == current && graph->isSingle()) ||
                         (!graph->isSingle() && (current != nullptr) && !current->isSingle()))
                {
                    // if it's the current single graph or both are parallel...
                    if (graphChanged && (graph->isSingle() || 
                                        (modeChanged && !graph->isSingle() && !current->isSingle())))
                    {
                        // DBG("  FADE IN NEW GRAPH: " << graph->engineIndex);
                        for (int i = 0; i < numOutputChans; ++i)
                            audioOut.addFromWithRamp (i, 0, audioTemp.getReadPointer (i), 
                                                      numSamples, 0.f, 1.f);
                    }
                    else
                    {
                        for (int i = 0; i < numOutputChans; ++i)
                            audioOut.addFrom (i, 0, audioTemp, i, 0, numSamples);
                    }
                    
                    midiOut.addEvents (midiTemp, 0, numSamples, 0);
                }
            }

            for (int i = 0; i < numChans; ++i)
                buffer.copyFrom (i, 0, audioOut, i, 0, numSamples);

            MidiBuffer::Iterator iter (midi);
            MidiMessage msg; int frame = 0;
            
            // setup a program change if present
           #if defined (EL_PRO)
            while (iter.getNextEvent (msg, frame) && frame < numSamples)
            {
                if (! msg.isProgramChange())
                    continue;
                program.program = msg.getProgramChangeNumber();
                program.channel = msg.getChannel();
            }
           #endif // EL_PRO

            // done with input, swap it with the rendered output
            midi.swapWith (midiOut);
        }
        else
        {
            midi.clear();
            for (int i = 0; i < buffer.getNumChannels(); ++i)
                zeromem (buffer.getWritePointer(i), sizeof (float) * (size_t) numSamples);
        }

        lastGraph = currentGraph;
    }
    
    /** not realtime safe! */

    bool addGraph (RootGraph* graph)
    {
        graph->setLocked (locked);
        graphs.add (graph);
        graph->engineIndex = graphs.size() - 1;

        if (graph->engineIndex == 0)
        {
            setCurrentGraph (0);
            lastGraph = 0;
        }
        
        return true;
    }

    /** not realtime safe! AudioEngine's callback should be locked when you call this */
    void removeGraph (RootGraph* graph)
    {
        jassert (graphs.contains (graph));
        graphs.removeFirstMatchingValue (graph);
        graph->engineIndex = -1;
        updateIndexes();
        if (currentGraph >= graphs.size())
            currentGraph = graphs.size() - 1;
        if (lastGraph >= graphs.size())
            lastGraph = graphs.size() - 1;
    }

    int size() const { return graphs.size(); }

    RootGraph* getGraph (const int i) const { return graphs.getUnchecked (i); }
    int getGraphIndex() const { return currentGraph; }
    const Array<RootGraph*>& getGraphs() const { return graphs; }
    
    /** passing in true turns off all rendering features in the paid version */
    void setLocked (const bool l)
    {
        locked = l;
        for (auto* g : graphs)
            g->setLocked (locked);
    }

private:
    Array<RootGraph*> graphs;
    bool locked             = false;
    int currentGraph        = -1;
    int lastGraph           = -1;

    struct ProgramRequest
    {
        int program      = -1;
        int channel      = -1;

        const bool wasRequested() const { return program >= 0; }
        void reset()
        {
            program = channel = -1;
        }

    } program;

    int numInputChans       = -1;
    int numOutputChans      = -1;
    AudioSampleBuffer   audioOut, audioTemp;

    MidiBuffer midiOut, midiTemp;

    void updateIndexes()
    {
        for (int i = 0 ; i < graphs.size(); ++i)
            graphs.getUnchecked(i)->engineIndex = i;
    }

    int findGraphForProgram (const ProgramRequest& r) const
    {
        if (isPositiveAndBelow (program.program, 128))
        {
            for (int i = 0; i < graphs.size(); ++i)
            {
                auto* const g = graphs.getUnchecked (i);
                if (g->midiProgram == r.program && g->acceptsMidiChannel (program.channel))
                    return g->engineIndex;
            }
        }

        return currentGraph;
    }
};

class AudioEngine::Private : public AudioIODeviceCallback,
                             public MidiInputCallback,
                             public Value::Listener,
                             public MidiClock::Listener,
                             public Timer
{
public:
    Private (AudioEngine& e)
        : engine (e),sampleRate (0), blockSize (0), isPrepared (false),
          numInputChans (0), numOutputChans (0),
          tempBuffer (1, 1)
    {
        tempoValue.addListener (this);
        externalClockValue.addListener (this);
        currentGraph.set (-1);
        processMidiClock.set (0);
        sessionWantsExternalClock.set (0);
        midiClock.addListener (this);
        graphs.onActiveGraphChanged = std::bind (&AudioEngine::Private::onCurrentGraphChanged, this);
        midiIOMonitor = new MidiIOMonitor();
        startTimerHz (90);
    }

    ~Private()
    {
        graphs.onActiveGraphChanged = nullptr;
        midiClock.removeListener (this);
        tempoValue.removeListener (this);
        externalClockValue.removeListener (this);
        
        if (isPrepared)
        {
            jassertfalse;
            releaseResources();
            isPrepared = false;
        }
    }
    
    void timerCallback() override
    {
        midiIOMonitor->notify();
    }

    RootGraph* getCurrentGraph() const { return graphs.getCurrentGraph(); }
    
    void onCurrentGraphChanged()
    {
        int currentGraph = -1;
        {
            ScopedLock sl (lock);
            currentGraph = graphs.getGraphIndex();
        }

        auto session = engine.getWorld().getSession();
        if (currentGraph >= 0 && currentGraph != session->getActiveGraphIndex())
        {
            // NOTE: this is a cheap way to refresh the GUI, in the future this
            // will need to be smarter by determining whether or not EC needs to
            // handle the change at the model layer.
            auto graphs = session->getValueTree().getChildWithName (Tags::graphs);
            graphs.setProperty (Tags::active, currentGraph, nullptr);
        }
    }
    
    void audioDeviceIOCallback (const float** const inputChannelData, const int numInputChannels,
                                float** const outputChannelData, const int numOutputChannels,
                                const int numSamples) override
    {
        jassert (sampleRate > 0 && blockSize > 0);
        int totalNumChans = 0;
        ScopedNoDenormals denormals;
        if (numInputChannels > numOutputChannels)
        {
            // if there aren't enough output channels for the number of
            // inputs, we need to create some temporary extra ones (can't
            // use the input data in case it gets written to)
            tempBuffer.setSize (numInputChannels - numOutputChannels, numSamples,
                                false, false, true);
            
            for (int i = 0; i < numOutputChannels; ++i)
            {
                channels[totalNumChans] = outputChannelData[i];
                memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
                ++totalNumChans;
            }
            
            for (int i = numOutputChannels; i < numInputChannels; ++i)
            {
                channels[totalNumChans] = tempBuffer.getWritePointer (i - numOutputChannels, 0);
                memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
                ++totalNumChans;
            }
        }
        else
        {
            for (int i = 0; i < numInputChannels; ++i)
            {
                channels[totalNumChans] = outputChannelData[i];
                memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
                ++totalNumChans;
            }
            
            for (int i = numInputChannels; i < numOutputChannels; ++i)
            {
                channels[totalNumChans] = outputChannelData[i];
                zeromem (channels[totalNumChans], sizeof (float) * (size_t) numSamples);
                ++totalNumChans;
            }
        }

        const bool wasPlaying = transport.isPlaying();
        AudioSampleBuffer buffer (channels, totalNumChans, numSamples);
        processCurrentGraph (buffer, incomingMidi);

        {
            ScopedLock lockMidiOut (engine.world.getMidiEngine().getMidiOutputLock());
            if (auto* const midiOut = engine.world.getMidiEngine().getDefaultMidiOutput())
            {
                if (sendMidiClockToInput.get() != 1 && generateMidiClock.get() == 1)
                {
                    if (wasPlaying != transport.isPlaying())
                    {
                        if (transport.isPlaying())
                        {
                            incomingMidi.addEvent (transport.getPositionFrames() <= 0 
                                ? MidiMessage::midiStart() : MidiMessage::midiContinue(), 0);
                        }
                        else
                        {
                            incomingMidi.addEvent (MidiMessage::midiStop(), 0);
                        }
                    }

                    midiClockMaster.setTempo (transport.getTempo());
                    midiClockMaster.render (incomingMidi, numSamples);
                }

                const double delayMs = midiOutLatency.get();
                if (! incomingMidi.isEmpty())
                {
                    midiIOMonitor->sent();
                    midiOut->sendBlockOfMessages (incomingMidi, delayMs + Time::getMillisecondCounterHiRes(), sampleRate);
                }
            }
        }
        
        incomingMidi.clear();
    }
    
    void processCurrentGraph (AudioBuffer<float>& buffer, MidiBuffer& midi)
    {
        const int numSamples = buffer.getNumSamples();
        messageCollector.removeNextBlockOfMessages (midi, numSamples);
        
        const ScopedLock sl (lock);
        const bool shouldProcess = shouldBeLocked.get() == 0;
        const bool wasPlaying = transport.isPlaying();
        transport.preProcess (numSamples);

        if (shouldProcess)
        {
            if (generateMidiClock.get() == 1 && sendMidiClockToInput.get() == 1)
            {
                if (wasPlaying != transport.isPlaying())
                {
                    if (transport.isPlaying())
                    {
                        midi.addEvent (transport.getPositionFrames() <= 0 
                            ? MidiMessage::midiStart() : MidiMessage::midiContinue(), 0);
                    }
                    else
                    {
                        midi.addEvent (MidiMessage::midiStop(), 0);
                    }
                }
                midiClockMaster.setTempo (static_cast<double> (transport.getTempo()));
                midiClockMaster.render (midi, numSamples);
            }

            if (currentGraph.get() != graphs.getCurrentGraphIndex())
                graphs.setCurrentGraph (currentGraph.get());
            graphs.renderGraphs (buffer, midi);  // user requested index can be cancelled by program changed
            currentGraph.set (graphs.getCurrentGraphIndex());
        }
        else
        {
            for (int i = 0; i < buffer.getNumChannels(); ++i)
                zeromem (buffer.getWritePointer(i), sizeof (float) * (size_t) numSamples);
        }

        if (transport.isPlaying())
            transport.advance (numSamples);
        
        transport.postProcess (numSamples);
    }
    
    bool isTimeMaster() const
    {
        if (engine.getRunMode() == RunMode::Plugin)
            return sessionWantsExternalClock.get() == 0;
        return processMidiClock.get() == 0 && sessionWantsExternalClock.get() == 0;
    }
    
    void audioDeviceAboutToStart (AudioIODevice* const device) override
    {
        const double newSampleRate = device->getCurrentSampleRate();
        const int newBlockSize     = device->getCurrentBufferSizeSamples();
        const int numChansIn       = device->getActiveInputChannels().countNumberOfSetBits();
        const int numChansOut      = device->getActiveOutputChannels().countNumberOfSetBits();
        audioAboutToStart (newSampleRate, newBlockSize, numChansIn, numChansOut);
    }
    
    void audioAboutToStart (const double newSampleRate, const int newBlockSize,
                            const int numChansIn, const int numChansOut)
    {
        const ScopedLock sl (lock);
        
        sampleRate      = newSampleRate;
        blockSize       = newBlockSize;
        numInputChans   = numChansIn;
        numOutputChans  = numChansOut;
        
        midiClock.reset (sampleRate, blockSize);
        messageCollector.reset (sampleRate);
        keyboardState.addListener (&messageCollector);
        channels.calloc ((size_t) jmax (numChansIn, numChansOut) + 2);
        
        graphs.prepareBuffers (numInputChans, numOutputChans, blockSize);

        if (isPrepared)
        {
            isPrepared = false;
            releaseResources();
        }

        prepareToPlay (sampleRate, blockSize);
        isPrepared = true;
    }
    
    void audioDeviceStopped() override
    {
        audioStopped();
    }
    
    void audioStopped()
    {
        const ScopedLock sl (lock);
        keyboardState.removeListener (&messageCollector);
        if (isPrepared)
            releaseResources();
        isPrepared  = false;
        sampleRate  = 0.0;
        blockSize   = 0;
        tempBuffer.setSize (1, 1);
        graphs.releaseBuffers();
    }
    
    void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override
    {
        if (! message.isActiveSense() && ! message.isMidiClock())
            midiIOMonitor->received();
        messageCollector.addMessageToQueue (message);
        const bool clockWanted = processMidiClock.get() > 0 && sessionWantsExternalClock.get() > 0;
        if (clockWanted && message.isMidiClock())
        {
            midiClock.process (message);
        }
        else if (clockWanted && message.isMidiStart())
        {
            transport.requestPlayState (true);
            transport.requestAudioFrame (0);
        }
        else if (clockWanted && message.isMidiStop())
        {
            transport.requestPlayState (false);
        }
        else if (clockWanted && message.isMidiContinue())
        {
            transport.requestPlayState (true);
        }   
    }
    
    void addGraph (RootGraph* graph)
    {
        jassert (graph);
        if (isPrepared)
            prepareGraph (graph, sampleRate, blockSize);
        ScopedLock sl (lock);
        if (graphs.addGraph (graph))
        {
            graph->renderingSequenceChanged.connect (
                std::bind (&AudioEngine::updateExternalLatencySamples, &engine));
        }
    }
    
    void removeGraph (RootGraph* graph)
    {
        {
            ScopedLock sl (lock);
            graphs.removeGraph (graph);
        }
        
        graph->renderingSequenceChanged.disconnect_all_slots();
        if (isPrepared)
            graph->releaseResources();
    }
    
    void connectSessionValues()
    {
        if (session)
        {
            tempoValue.referTo (session->getPropertyAsValue (Tags::tempo));
            externalClockValue.referTo (session->getPropertyAsValue ("externalSync"));
            transport.requestMeter (session->getProperty (Tags::beatsPerBar, 4),
                                    session->getProperty (Tags::beatDivisor, 2));
        }
        else
        {
            tempoValue = tempoValue.getValue();
            externalClockValue = externalClockValue.getValue();
        }
    }
    
    void setSession (SessionPtr s)
    {
        session = s;
        connectSessionValues();
    }
    
    void valueChanged (Value& value) override
    {
        if (tempoValue.refersToSameSourceAs (value))
        {
            const float tempo = (float) tempoValue.getValue();
            if (sessionWantsExternalClock.get() <= 0 || processMidiClock.get() <= 0)
                transport.requestTempo (tempo);
        }
        else if (externalClockValue.refersToSameSourceAs (value))
        {
            const bool wantsClock = (bool)value.getValue();
            if (wantsClock)
            {
                resetMidiClock();
            }
            else
            {
                transport.requestTempo ((float) tempoValue.getValue());
            }
            
            sessionWantsExternalClock.set (wantsClock ? 1 : 0);
        }
    }
    
    void resetMidiClock()
    {
        midiClock.reset (sampleRate, blockSize);
    }
    
    void midiClockTempoChanged (const float bpm) override
    {
        if (sessionWantsExternalClock.get() > 0 && processMidiClock.get() > 0)
            transport.requestTempo (bpm);
    }
    
    void midiClockSignalAcquired()  override { }
    void midiClockSignalDropped()   override { }
    
    bool isUsingExternalClock() const
    {
        if (engine.getRunMode() == RunMode::Plugin)
            return sessionWantsExternalClock.get() > 0;
        return sessionWantsExternalClock.get() > 0 && processMidiClock.get() > 0;
    }
    
private:
    friend class AudioEngine;
    AudioEngine&        engine;
    Transport           transport;
    RootGraphRender     graphs;
    SessionPtr          session;
    
    Value tempoValue;
    Atomic<float> nextTempo;
    
    CriticalSection     lock;
    double sampleRate   = 0.0;
    int blockSize       = 0;
    bool isPrepared     = false;
    Atomic<int> currentGraph;

    int numInputChans, numOutputChans;
    HeapBlock<float*> channels;
    AudioSampleBuffer tempBuffer;
    MidiBuffer incomingMidi;
    MidiMessageCollector messageCollector;
    MidiKeyboardState keyboardState;

    AudioSampleBuffer graphBuffer;
    AudioSampleBuffer graphMixBuffer;

    Value externalClockValue;
    Atomic<int> sessionWantsExternalClock;
    Atomic<int> processMidiClock;
    Atomic<int> generateMidiClock { 0 };
    Atomic<int> sendMidiClockToInput { 0 };

    MidiClock midiClock;
    MidiClockMaster midiClockMaster;
    
    AudioPlayHead::CurrentPositionInfo hostPos, lastHostPos;
    
    int latencySamples = 0;
   
    // GraphRender::locked must match this default value
    Atomic<int> shouldBeLocked { 0 };

    MidiIOMonitorPtr midiIOMonitor;

    Atomic<double> midiOutLatency { 0.0 };

    void prepareGraph (RootGraph* graph, double sampleRate, int estimatedBlockSize)
    {
        graph->setPlayConfigDetails (numInputChans, numOutputChans,
                                     sampleRate, blockSize);
        graph->setPlayHead (&transport);
        graph->prepareToPlay (sampleRate, estimatedBlockSize);
    }
    
    void prepareToPlay (double sampleRate, int estimatedBlockSize)
    {
        midiClockMaster.setSampleRate (sampleRate);
        midiClockMaster.setTempo (transport.getTempo());
        for (int i = 0; i < graphs.size(); ++i)
            prepareGraph (graphs.getGraph(i), sampleRate, estimatedBlockSize);
    }
    
    void releaseResources()
    {
        for (int i = 0; i < graphs.size(); ++i)
            graphs.getGraph(i)->releaseResources();
    }
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Private)
};

AudioEngine::AudioEngine (Globals& g, RunMode m)
    : world (g), runMode (m)
{
    priv = new Private (*this);
}

AudioEngine::~AudioEngine() noexcept
{
    deactivate();
    priv = nullptr;
}

void AudioEngine::activate()
{
    if (getRunMode() == RunMode::Standalone)
    {
        auto& midi (world.getMidiEngine());
        midi.addMidiInputCallback (String(), &getMidiInputCallback());
    }
}

void AudioEngine::deactivate()
{
    if (getRunMode() == RunMode::Standalone)
    {
        auto& midi (world.getMidiEngine());
        midi.removeMidiInputCallback (String(), &getMidiInputCallback());
    }
}

AudioIODeviceCallback&  AudioEngine::getAudioIODeviceCallback() { jassert (priv != nullptr); return *priv; }
MidiInputCallback&      AudioEngine::getMidiInputCallback()     { jassert (priv != nullptr); return *priv; }

bool AudioEngine::addGraph (RootGraph* graph)
{
    jassert (priv && graph);
    priv->addGraph (graph);
    return true;
}

void AudioEngine::applySettings (Settings& settings)
{
    const bool useMidiClock = settings.getUserSettings()->getValue("clockSource") == "midiClock";
    if (useMidiClock)
        priv->resetMidiClock();
    priv->processMidiClock.set (useMidiClock ? 1 : 0);
    priv->generateMidiClock.set (settings.generateMidiClock() ? 1 : 0);
    priv->sendMidiClockToInput.set (settings.sendMidiClockToInput() ? 1 : 0);
    priv->midiOutLatency.set (settings.getMidiOutLatency());
}

bool AudioEngine::removeGraph (RootGraph* graph)
{
    jassert (priv && graph);
    priv->removeGraph (graph);
    return true;
}

RootGraph* AudioEngine::getGraph (const int index)
{
    ScopedLock sl (priv->lock);
    if (isPositiveAndBelow (index, priv->graphs.size()))
        return priv->graphs.getGraph (index);
    return nullptr;
}

void AudioEngine::addMidiMessage (const MidiMessage msg, bool handleOnDeviceQueue)
{
    if (priv == nullptr)
        return;
    if (handleOnDeviceQueue)
        priv->handleIncomingMidiMessage (nullptr, msg);
    else
        priv->messageCollector.addMessageToQueue (msg);
}

void AudioEngine::setActiveGraph (const int index)
{
    if (priv == nullptr)
        return;
    priv->currentGraph.set (index);
}

int AudioEngine::getActiveGraph() const { return (priv != nullptr) ? priv->currentGraph.get() : -1; }

void AudioEngine::setSession (SessionPtr session)
{
    if (priv)
        priv->setSession (session);
}

void AudioEngine::refreshSession()
{
    if (priv)
        priv->connectSessionValues();
}

MidiKeyboardState& AudioEngine::getKeyboardState()
{
    jassert(priv);
    return priv->keyboardState;
}
    
Transport::MonitorPtr AudioEngine::getTransportMonitor() const
{
    return (priv != nullptr) ? priv->transport.getMonitor() : nullptr;
}

void AudioEngine::setMeter (int beatsPerBar, int beatDivisor)
{
    auto& transport (priv->transport);
    transport.requestMeter (beatsPerBar, beatDivisor);
}

void AudioEngine::togglePlayPause()
{
    auto& transport (priv->transport);
    transport.requestPlayPause();
}

void AudioEngine::setPlaying (const bool shouldBePlaying)
{
    auto& transport (priv->transport);
    transport.requestPlayState (shouldBePlaying);
}

void AudioEngine::setRecording (const bool shouldBeRecording)
{
    auto& transport (priv->transport);
    transport.requestRecordState (shouldBeRecording);
}

void AudioEngine::seekToAudioFrame (const int64 frame)
{
    auto& transport (priv->transport);
    transport.requestAudioFrame (frame);
}

void AudioEngine::prepareExternalPlayback (const double sampleRate, const int blockSize,
                                           const int numIns, const int numOuts)
{
    if (priv)
        priv->audioAboutToStart (sampleRate, blockSize, numIns, numOuts);
}

void AudioEngine::processExternalBuffers (AudioBuffer<float>& buffer, MidiBuffer& midi)
{
    if (priv)
    {
        if (getRunMode() == RunMode::Plugin)
            world.getMidiEngine().processMidiBuffer (midi, buffer.getNumSamples(), priv->sampleRate);
        priv->processCurrentGraph (buffer, midi);
    }
}

bool AudioEngine::isUsingExternalClock() const {
    return priv && priv->isUsingExternalClock();
}

void AudioEngine::processExternalPlayhead (AudioPlayHead* playhead, const int nframes)
{
    auto& pos (priv->hostPos);
    playhead->getCurrentPosition (pos);
    auto& transport (priv->transport);
    transport.requestTempo (pos.bpm);
    transport.requestMeter (pos.timeSigNumerator, BeatType::fromPosition (pos));
    transport.requestPlayState (pos.isPlaying);
    transport.requestRecordState (pos.isRecording);
    if (transport.getPositionFrames() != pos.timeInSamples)
        transport.requestAudioFrame (pos.timeInSamples);
    
    transport.preProcess (0);
    transport.postProcess (0);
}

void AudioEngine::releaseExternalResources()
{
    if (priv)
        priv->audioStopped();
}

Globals& AudioEngine::getWorld() const { return world; }

void AudioEngine::updateExternalLatencySamples()
{
    int latencySamples = 0;

    {
        ScopedLock sl (priv->lock);

        auto* current = priv->getCurrentGraph();
        if (nullptr == current) return;
    
        if (current->getRenderMode() == RootGraph::SingleGraph)
        {
            latencySamples = current->getLatencySamples();
        }
        else
        {
            for (auto* const graph : priv->graphs.getGraphs())
                if (graph->getRenderMode() == RootGraph::Parallel)
                    latencySamples = jmax (latencySamples, graph->getLatencySamples());
        }
    }

    priv->latencySamples = latencySamples;
    sampleLatencyChanged();
}

int AudioEngine::getExternalLatencySamples() const
{
    return priv != nullptr ? priv->latencySamples : 0;
}

MidiIOMonitorPtr AudioEngine::getMidiIOMonitor() const
{
    return priv != nullptr ? priv->midiIOMonitor : nullptr;
}

}
