/*!********************************************************************

Audacity: A Digital Audio Editor

@file MIDIPlay.h
@brief Inject added MIDI playback capability into Audacity's audio engine
 
Paul Licameli split from AudIOBase.h

**********************************************************************/

#ifndef __AUDACITY_MIDI_PLAY__
#define __AUDACITY_MIDI_PLAY__

#include "AudioIOExt.h"
#include <optional>
#include "WrapAllegro.h"

typedef void PmStream;
typedef int32_t PmTimestamp;
class Alg_event;
class Alg_iterator;
class NoteTrack;
using NoteTrackConstArray = std::vector < std::shared_ptr< const NoteTrack > >;

// This workaround makes pause and stop work when output is to GarageBand,
// which seems not to implement the notes-off message correctly.
#define AUDIO_IO_GB_MIDI_WORKAROUND

#include "NoteTrack.h"
#include "PlaybackSchedule.h"

namespace {

struct MIDIPlay;

Alg_update gAllNotesOff; // special event for loop ending
// the fields of this event are never used, only the address is important

struct Iterator {
   Iterator(
      const PlaybackSchedule &schedule, MIDIPlay &midiPlay,
      NoteTrackConstArray &midiPlaybackTracks,
      double startTime, double offset, bool send );
   ~Iterator();

   void Prime(bool send, double startTime);

   double GetNextEventTime() const;

   // Compute nondecreasing real time stamps, accounting for pauses, but not the
   // synth latency.
   double UncorrectedMidiEventTime(double pauseTime);

   bool Unmuted(bool hasSolo) const;

   // Returns true after outputting all-notes-off
   bool OutputEvent(double pauseTime,
      /// when true, sendMidiState means send only updates, not note-ons,
      /// used to send state changes that precede the selected notes
      bool sendMidiState,
      bool hasSolo);
   void GetNextEvent();

   const PlaybackSchedule &mPlaybackSchedule;
   MIDIPlay &mMIDIPlay;
   Alg_iterator it{ nullptr, false };
   /// The next event to play (or null)
   Alg_event    *mNextEvent = nullptr;

   /// Track of next event
   NoteTrack        *mNextEventTrack = nullptr;

   /// Is the next event a note-on?
   bool             mNextIsNoteOn = false;

private:
   /// Real time at which the next event should be output, measured in seconds.
   /// Note that this could be a note's time+duration for note offs.
   double           mNextEventTime = 0;
};

struct MIDIPlay : AudioIOExt
{
   explicit MIDIPlay(const PlaybackSchedule &schedule);
   ~MIDIPlay() override;

   double AudioTime(double rate) const
   { return mPlaybackSchedule.mT0 + mNumFrames / rate; }

   const PlaybackSchedule &mPlaybackSchedule;
   NoteTrackConstArray mMidiPlaybackTracks;

   /// True when output reaches mT1
   static bool      mMidiOutputComplete;

   /// mMidiStreamActive tells when mMidiStream is open for output
   static bool      mMidiStreamActive;

   PmStream        *mMidiStream = nullptr;
   int              mLastPmError = 0;

   /// Latency of MIDI synthesizer
   long             mSynthLatency = MIDISynthLatency_ms.GetDefault();

   // These fields are used to synchronize MIDI with audio:

   /// Number of frames output, including pauses
   long    mNumFrames = 0;
   /// total of backward jumps
   int     mMidiLoopPasses = 0;
   //
   inline double MidiLoopOffset() {
      return mMidiLoopPasses * (mPlaybackSchedule.mT1 - mPlaybackSchedule.mT0);
   }

   long    mAudioFramesPerBuffer = 0;
   /// Used by Midi process to record that pause has begun,
   /// so that AllNotesOff() is only delivered once
   bool    mMidiPaused = false;
   /// The largest timestamp written so far, used to delay
   /// stream closing until last message has been delivered
   PmTimestamp mMaxMidiTimestamp = 0;

   /// Offset from ideal sample computation time to system time,
   /// where "ideal" means when we would get the callback if there
   /// were no scheduling delays or computation time
   double mSystemMinusAudioTime = 0.0;
   /// audio output latency reported by PortAudio
   /// (initially; for Alsa, we adjust it to the largest "observed" value)
   double mAudioOutLatency = 0.0;

   // Next two are used to adjust the previous two, if
   // PortAudio does not provide the info (using ALSA):

   /// time of first callback
   /// used to find "observed" latency
   double mStartTime = 0.0;
   /// number of callbacks since stream start
   long mCallbackCount = 0;

   double mSystemMinusAudioTimePlusLatency = 0.0;

   std::optional<Iterator> mIterator;

#ifdef AUDIO_IO_GB_MIDI_WORKAROUND
   std::vector< std::pair< int, int > > mPendingNotesOff;
#endif

   void PrepareMidiIterator(bool send, double startTime, double offset);
   bool StartPortMidiStream(double rate);
   double PauseTime(double rate, unsigned long pauseFrames);
   void AllNotesOff(bool looping = false);

   /** \brief Compute the current PortMidi timestamp time.
    *
    * This is used by PortMidi to synchronize midi time to audio samples
    */
   PmTimestamp MidiTime();

   bool mUsingAlsa = false;

   static bool IsActive();
   bool IsOtherStreamActive() const override;

   void ComputeOtherTimings(double rate, bool paused,
      const PaStreamCallbackTimeInfo *timeInfo,
      unsigned long framesPerBuffer) override;
   void SignalOtherCompletion() override;
   unsigned CountOtherSolo() const override;

   bool StartOtherStream(const TransportSequences &tracks,
      const PaStreamInfo* info, double startTime, double rate) override;
   void AbortOtherStream() override;
   void FillOtherBuffers(double rate, unsigned long pauseFrames,
      bool paused, bool hasSolo) override;
   void StopOtherStream() override;

   AudioIODiagnostics Dump() const override;
};

}

#endif
