/*
      
 //#######################################################################################
 //Class to insert Midiclock messages into a given MidiBuffer, suitable for block
 //based audio callbacks. The class can act on position jumps e.g. "Loops" by sending a 
 //positioning message.
 //NOTE: No ballistik came in to play so I wouldn't recommend using it to drive a mechanical
 //tape deck!!!
 //
 //solar3d-software, April- 10- 2012
 //#######################################################################################
 
*/
#include "JK_MidiClock.h"
#include "juce_core/maths/juce_MathsFunctions.h"
#include <cmath>
#include <synth/synth_defs.h>


JK_MidiClock::JK_MidiClock() 
                        :m_wasPlaying(false),
                         m_syncPpqPosition(-999.0),
                         m_posChangeThreshold(0.001),
                         m_ppqToStartSyncAt(0.0),
                         m_followSongPosition(true),
                         m_syncFlag(0),
                         m_ppqOffset(0)
{
	//Prepare Midi messages to avoid blocking the caller of generateMidiclock()!
	m_continueMessage     = new MidiMessage(MidiMessage::midiContinue());
	m_stopMessage         = new MidiMessage(MidiMessage::midiStop());
	m_clockMessage        = new MidiMessage(MidiMessage::midiClock());
	m_songPositionMessage = new MidiMessage(MidiMessage::songPositionPointer(0));
    m_sample_remain = 0;
}
//=================================================================================================
void JK_MidiClock::generateMidiclock(const AudioPlayHead::CurrentPositionInfo &posInfo,
                                     MidiBuffer* midiBuffer, const int bufferSize, const double sampleRate)
{
    //###################################Some explanation about musical tempo #################
	//A Time Signature, is two numbers, one on top of the other. The numerator describes the  # 
	//number of Beats in a Bar, while the denominator describes of what note value a Beat is. #
    //So 4/4 would be four quarter-notes per Bar, while 4/2 would be four half-notes per Bar, #
	//4/8 would be four eighth-notes per Bar, and 2/4 would be two quarter-notes per Bar.     #
	//#########################################################################################

	if (midiBuffer == nullptr)
    {
        return;
    }

    //PPQ value of one sample
    const double ppqPerSample = (posInfo.bpm / 60.0) / sampleRate;
    
    //PPQ offset to compensate Midi interface latency
    double hostPpqPosition = posInfo.ppqPosition + m_ppqOffset * ppqPerSample;

	const double clockDistanceInSamples_f = (60.0 * sampleRate) / (posInfo.bpm * 24.0);
	const int clockDistanceInSamples = roundToInt(clockDistanceInSamples_f);

	const double quant = (60.0 / posInfo.bpm) * sampleRate;

    if (posInfo.isPlaying || posInfo.isRecording)
    {							         
        if (! m_wasPlaying)
        { 
            //set the point where to start the slave
            m_ppqToStartSyncAt = getNearestSixteenthInPPQ(hostPpqPosition);
            SynthDebug("m_ppqToStartSyncAt %f", m_ppqToStartSyncAt);
            
            //Special case: Master is set to always start playback from the previous start position... 
            if (positionJumped(m_syncPpqPosition, hostPpqPosition, sampleRate, ppqPerSample))
            {										
                //Cue Midiclock slave to the nearest sixteenth note to new start position
                //because the one calculated in stop mode isn't valid anymore.		
                sendSongPositionPointerMessage(m_ppqToStartSyncAt, 0, midiBuffer);
                SynthDebug("m_ppqToStartSyncAt %f", m_ppqToStartSyncAt);
            }
            m_sample_remain = clockDistanceInSamples;

        }
        else
        {
            //Position jump (loop or manually position change while playing)
            if (positionJumped(m_syncPpqPosition, hostPpqPosition, sampleRate, ppqPerSample))
            {										
                SynthDebug("positionJumped");
                //set the point where to start the slave
                m_ppqToStartSyncAt = getNearestSixteenthInPPQ(hostPpqPosition);
                
                //User has changed position manually while playing
                if (m_syncFlag == 0)
                {                   
                    SynthDebug("Sync=0");
                    midiBuffer->addEvent(*m_stopMessage, 0);
                    sendSongPositionPointerMessage(hostPpqPosition, 0, midiBuffer);
                        
                    m_syncFlag = startSlave_;
                } 
                else
                {                   
                    SynthDebug("Sync=1");
                    if (m_followSongPosition)
                    {
                        sendSongPositionPointerMessage(hostPpqPosition, 0, midiBuffer);
                        
                        m_syncFlag = startSlave_;
                    }
                    else
                    {
                        m_syncFlag = 0;
                    }
                }
                m_sample_remain = clockDistanceInSamples;
            }
        }

        for (int posInBuffer = 0; posInBuffer < bufferSize; ++posInBuffer) 
        {            
            m_syncPpqPosition = hostPpqPosition + (posInBuffer * ppqPerSample);
            
            //Some hosts like Cubase come up with a wacky ppqPosition
            //that could break the timing! Best is to "wait" 
            //here for the right ppqPosition to jump on.
            if (m_syncPpqPosition >= m_ppqToStartSyncAt)
            {                             
                if ((m_syncFlag & startSlave_) == startSlave_)
                {
                    midiBuffer->addEvent(*m_continueMessage, posInBuffer);
                    
                    m_syncFlag &= cycleEnd_;
                }
                
                //Cycle mode on
                if (posInfo.isLooping && posInfo.ppqLoopStart != posInfo.ppqLoopEnd)
                {
                    const double ppqToCycleEnd = fabs(posInfo.ppqLoopEnd - m_syncPpqPosition);
                    const int64 samplesToCycleEnd = roundToInt64(ppqToCycleEnd * (60.0 / posInfo.bpm) * sampleRate);
                    
                    if ((m_syncFlag & cycleEnd_) == 0)
                    {
                        if (samplesToCycleEnd <= clockDistanceInSamples) //For fine tuning tweak here
                        {													
                            //We have reached the cycle- end position 
                            //and must stop the Midiclock slave here
                            if (m_followSongPosition)
                                midiBuffer->addEvent(*m_stopMessage, posInBuffer);
                            
                            m_syncFlag |= cycleEnd_;
                        }
                    }                   
                }
            }
        }

        //For best timing we should never interupt Midiclock messages!
        //Seems that some slaves constantly adjusting their internal clock
        //to Midiclock even if they are in stop mode.
        int buffer_remain = bufferSize;
        int posInBuffer = 0;
        int buffer_consume = 0;
        while(buffer_remain)
        {
            if (m_sample_remain > buffer_remain)
            {
                m_sample_remain -= buffer_remain;
                buffer_consume = buffer_remain;
            }
            else
            {
                buffer_consume = m_sample_remain;
                m_sample_remain = 0;
            }
            
            buffer_remain -= buffer_consume;
            posInBuffer += buffer_consume;

            if (buffer_remain == 0)
            {
                break;
            }

            if (m_sample_remain == 0)
            {
                m_sample_remain = clockDistanceInSamples;
#if 0                
                SynthDebug("posInBuffer                     : %d", posInBuffer);
                SynthDebug("clockDistanceInSamples_f (soll) : %f", clockDistanceInSamples_f);
                SynthDebug("hostPpqPosition                 : %f", hostPpqPosition);
                SynthDebug("hostPpqPosition  (Q)            : %f", getNearestInPPQ(hostPpqPosition, 96));
                SynthDebug("hostSamplePos    (Q)            : %f", getNearestInPPQ(hostPpqPosition, 96) * quant);
#endif
                midiBuffer->addEvent(*m_clockMessage, roundToInt(posInBuffer));
            }
        }
        
        m_wasPlaying = true;
    }
    else
    {	        
        //Send positioning message if the user has stopped or if he changed the playhead position  
        //manually in stop mode! This will also initially cue slave after loading plugin instance.
        if (m_wasPlaying || positionJumped(m_syncPpqPosition, hostPpqPosition, sampleRate, ppqPerSample))
        {	            
            midiBuffer->addEvent(*m_stopMessage, 0);				
            sendSongPositionPointerMessage(hostPpqPosition, 0, midiBuffer);
        }
        
        m_syncPpqPosition = hostPpqPosition;
        m_syncFlag = startSlave_;
        m_wasPlaying = false;

    }
}	

//=================================================================================================
bool JK_MidiClock::positionJumped(const double lastPosInPPQ, const double currentPosInPPQ, 
                                  const double sampleRate, const double ppqPerSample)
{
	//This returns true if the user has changed the playhead position manually or if 
	//a jump has occured! The comperator's default threshold is lastPosInPPQ +- 10ms.
	if (currentPosInPPQ < lastPosInPPQ - ((m_posChangeThreshold * sampleRate) * ppqPerSample) ||
		currentPosInPPQ > lastPosInPPQ + ((m_posChangeThreshold * sampleRate) * ppqPerSample))
        return true;
    
	return false;
}
//=================================================================================================
void JK_MidiClock::sendSongPositionPointerMessage(const double ppqPosition, const int posInBuffer, MidiBuffer* buffer)
{
	//This will cue the slave to the NEAREST 
	//16th note to the given ppqPosition.
	int intBeat = int(ceil(ppqPosition * 4));
    
	uint8* pSongPositionTime((uint8*)(m_songPositionMessage->getRawData()));
	
	*(pSongPositionTime + 1) = (uint8)(intBeat & 0x7f);
	*(pSongPositionTime + 2) = (uint8)((intBeat & 0x3f80)>>7);
    
	buffer->addEvent(*m_songPositionMessage, posInBuffer);
}
