//| Copyright: (C) 2020-2024 Kevin Larke <contact AT larke DOT org> 
//| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
#include "cwCommon.h"
#include "cwLog.h"
#include "cwCommonImpl.h"
#include "cwTest.h"
#include "cwMem.h"
#include "cwTime.h"
#include "cwFile.h"
#include "cwObject.h"
#include "cwFileSys.h"
#include "cwText.h"
#include "cwTextBuf.h"
#include "cwMidi.h"
#include "cwMidiDecls.h"
#include "cwMidiFile.h"
#include "cwMidiDevice.h"
#include <poll.h>
#include "cwMidiFileDev.h"

namespace cw
{
  namespace midi
  {
    namespace device
    {
      namespace file_dev
      {

        typedef struct file_msg_str
        {
          unsigned long long      amicro;   // 
          msg_t*                  msg;      // msg_t as declared in cwMidiDecls.h
          unsigned                file_idx;
          unsigned                msg_idx;
        } file_msg_t;
      
        
        typedef struct file_str
        {
          char*                label;
          char*                fname;
          
          msg_t*               msgA;
          unsigned             msgN;
          
          bool                 enable_fl;
        } file_t;
      
        typedef struct file_dev_str
        {
          cbFunc_t         cbFunc;
          void*            cbArg;
          
          file_t*          fileA;      // fileA[ fileN ]
          unsigned         fileN;      //
        
          file_msg_t*      msgA;       // msgA[ msgN ]
          unsigned         msgAllocN;  //
          unsigned         msgN; 

          unsigned         devCnt;     // always 1
          unsigned         base_dev_idx;
          char*            dev_name;

          bool             is_activeFl;

          // The following indexes are all int p->msgA[ p->msgN ]
          unsigned         beg_msg_idx;      // beg_msg_idx is the first msg to transmit
          unsigned         end_msg_idx;      // end_msg_idx indicates the last msg to transmit, end_msg_idx+1 will not be transmitted
          unsigned         next_wr_msg_idx;  // next_wr_msg_idx is the next msg to transmit
          unsigned         next_rd_msg_idx;  // next_rd_msg_idx is the last msg transmitted

          unsigned long long   start_delay_micros;
          
          unsigned long long read_ahead_micros;

          bool                  latency_meas_enable_in_fl;
          bool                  latency_meas_enable_out_fl;
          latency_meas_result_t latency_meas_result;
          
        } file_dev_t;

        file_dev_t * _handleToPtr(handle_t h)
        { return handleToPtr<handle_t,file_dev_t>(h); }

        rc_t _validate_file_index(file_dev_t* p, unsigned file_idx)
        {
          rc_t rc = kOkRC;
        
          if( file_idx >= p->fileN )
            rc = cwLogError(kInvalidArgRC,"The MIDI device file/port index %i is invalid.",file_idx);
        
          return rc;
        }

        bool _file_exists( const file_t& r )
        { return r.msgN > 0; }

        rc_t _validate_file_existence(file_dev_t* p, unsigned file_idx )
        {
          rc_t rc;
          if((rc = _validate_file_index(p,file_idx)) == kOkRC )
            if( !_file_exists(p->fileA[file_idx]) )
              rc = cwLogError(kInvalidArgRC,"The MIDI device file at file/port index %i does not exist.",file_idx);
            
          return rc;
        }

        rc_t _validate_dev_index(file_dev_t* p, unsigned dev_idx )
        {
          rc_t rc = kOkRC;
          
          if( dev_idx >= p->devCnt )
            rc = cwLogError(kInvalidArgRC,"The MIDI file device index %i is invalid.",dev_idx );
          return rc;
        }

        rc_t _validate_port_index(file_dev_t* p, unsigned port_idx )
       { return _validate_file_index(p,port_idx); }

        void _reset_indexes( file_dev_t* p )
        {
          p->beg_msg_idx     = kInvalidIdx;
          p->end_msg_idx     = kInvalidIdx;
          p->next_wr_msg_idx = kInvalidIdx;
          p->next_rd_msg_idx = kInvalidIdx;          
        }
        
        rc_t _close_file( file_dev_t* p, unsigned file_idx )
        {
          rc_t rc = kOkRC;

          
          if((rc = _validate_file_index(p,file_idx)) != kOkRC )
            goto errLabel;

          if( _file_exists(p->fileA[file_idx] ) )
          {
            // if the beg/end msg index refers to the file being closed then invalidate the beg/end msg idx
            if( p->beg_msg_idx != kInvalidIdx && p->beg_msg_idx < p->msgN && p->msgA[ p->beg_msg_idx ].file_idx == file_idx )
              p->beg_msg_idx = kInvalidIdx;
            
            if( p->end_msg_idx != kInvalidIdx && p->end_msg_idx < p->msgN && p->msgA[ p->end_msg_idx ].file_idx == file_idx )
              p->end_msg_idx = kInvalidIdx;

            mem::release(p->fileA[file_idx].fname);
            p->fileA[file_idx].msgN = 0;
          }

          
        errLabel:
          return rc;
        }
      
        rc_t _destroy( file_dev_t* p )
        {
          rc_t rc = kOkRC;

          for(unsigned i=0; i<p->fileN; ++i)
          {
            mem::release(p->fileA[i].msgA);          
            mem::release(p->fileA[i].label);
            mem::release(p->fileA[i].fname);
          }

          mem::release(p->fileA);
          mem::release(p->msgA);
          mem::release(p->dev_name);
          mem::release(p);
          //errLabel:
          return rc;
        }


        rc_t _open_midi_file( file_dev_t* p, unsigned file_idx, const char* fname )
        {
          rc_t                 rc = kOkRC;
          midi::file::handle_t mfH;
          
          if((rc = open(mfH, fname)) != kOkRC )
          {
            rc = cwLogError(rc,"MIDI file open failed on '%s'.",cwStringNullGuard(fname));
            goto errLabel;
          }
          else
          {
            unsigned                 msg_idx     = 0;
            unsigned                 msgN        = msgCount(mfH);;
            const file::trackMsg_t** fileMsgPtrA = msgArray(mfH);
            
            p->fileA[file_idx].msgA = mem::resizeZ<msg_t>(p->fileA[file_idx].msgA,msgN);
            
            for(unsigned j=0; j<msgN; ++j)
              if( isChStatus(fileMsgPtrA[j]->status) )
              {
                msg_t* m = p->fileA[file_idx].msgA + msg_idx;
                
                m->uid       = j; 
                m->ch        = fileMsgPtrA[j]->u.chMsgPtr->ch;
                m->status    = fileMsgPtrA[j]->status;
                m->d0        = fileMsgPtrA[j]->u.chMsgPtr->d0;
                m->d1        = fileMsgPtrA[j]->u.chMsgPtr->d1;
                m->timeStamp = time::microsecondsToSpec(fileMsgPtrA[j]->amicro); 

                msg_idx += 1;
              }

            p->fileA[file_idx].msgN  = msg_idx;
            p->fileA[file_idx].fname = mem::duplStr(fname);
            close( mfH );

          }
          
          
        errLabel:
          return rc;
        }
    
        
        unsigned _calc_msg_count( file_dev_t* p )
        {
          unsigned msgAllocN = 0;
          for(unsigned i=0; i<p->fileN; ++i)
            if( p->fileA[i].msgA != nullptr )
              msgAllocN += p->fileA[i].msgN;
        
          return msgAllocN;          
        }

        // Set msg_idx to kInvalidIdx to seek to the current value of p->beg_msg_idx
        // or 0 if p->beg_msg_idx was never set.
        // If msg_idx is a valid msg index then it will be assigned to p->beg_msg_idx
        rc_t _seek_to_msg_index( file_dev_t* p, unsigned file_idx, unsigned msg_idx )
        {
          rc_t rc;
          unsigned i = 0;
          unsigned beg_msg_idx = p->beg_msg_idx;
          
          if((rc = _validate_file_existence(p,file_idx)) != kOkRC )
            goto errLabel;

          // if no target msg was given ...
          if( msg_idx == kInvalidIdx )
          {
            // ... then use the previous target msg or 0
            beg_msg_idx = p->beg_msg_idx == kInvalidIdx ? 0 : p->beg_msg_idx;
          }
          else // if a target msg was given ..
          {
            // locate the msg in p->msgA[]
            for(i=0; i<p->msgN; ++i)
              if( p->msgA[i].file_idx == file_idx && p->msgA[i].msg_idx == msg_idx )
              {
                p->beg_msg_idx = i;
                beg_msg_idx    = i;
                break;
              }

            if( i == p->msgN )
            {
              rc = cwLogError(kEleNotFoundRC,"The 'begin' MIDI file event at index %i in %s was not found.",msg_idx,cwStringNullGuard(p->fileA[file_idx].label));
              goto errLabel;
            }
          }

          p->next_wr_msg_idx = beg_msg_idx;
          p->next_rd_msg_idx = beg_msg_idx;
          
        errLabel:
          return rc;
        }

        // Set file_idx and msg_idx to kInvalidIdx to make the p->msgN the end index.
        // Set msg_idx to kInvalidIdx to make the last p->fileA[file_idx].msgN the end index.
        rc_t _set_end_msg_index( file_dev_t* p, unsigned file_idx, unsigned msg_idx )
        {
          rc_t rc = kOkRC;
          unsigned i = 0;

          if( file_idx == kInvalidIdx )
            p->end_msg_idx = p->msgN - 1;
          else
          {                    
            if((rc = _validate_file_existence(p,file_idx)) != kOkRC )
              goto errLabel;

            if( msg_idx == kInvalidIdx )
              msg_idx = p->fileA[ file_idx ].msgN - 1;
            
            // locate the msg in p->msgA[]
            for(i=0; i<p->msgN; ++i)
              if( p->msgA[i].file_idx == file_idx && p->msgA[i].msg_idx == msg_idx )
              {
                p->end_msg_idx = i;
                break;
              }

            if( i == p->msgN )
            {
              rc = cwLogError(kEleNotFoundRC,"The 'end' MIDI file event at index %i in %s was not found.",msg_idx,cwStringNullGuard(p->fileA[file_idx].label));
              goto errLabel;
            }
          }

        errLabel:          
          return rc;
        }

        
        unsigned _fill_msg_array_from_msg( file_dev_t* p, unsigned file_idx, msg_t* msgA, unsigned msgN, unsigned msg_idx )
        {
          unsigned  k  = msg_idx;
            
          for(unsigned j=0; j<msgN; ++j)
            if( isChStatus(msgA[j].status) )
            {
              p->msgA[k].msg_idx  = j;
              p->msgA[k].amicro   = time::specToMicroseconds(msgA[j].timeStamp);
              p->msgA[k].msg      = msgA + j;
              p->msgA[k].file_idx = file_idx;
              ++k;
            }
          
          return k;
          
        }
        
        void _fill_msg_array( file_dev_t* p )
        {
          unsigned msg_idx = 0;
          for(unsigned i=0; i<p->fileN; ++i)
            if( p->fileA[i].msgA != nullptr )
              msg_idx = _fill_msg_array_from_msg(p,i,p->fileA[i].msgA,p->fileA[i].msgN,msg_idx);

          p->msgN = msg_idx;
          
        }

        // Combine all the file msg's into a single array and sort them on file_msg_t.amicro.
        rc_t _prepare_msg_array( file_dev_t* p )
        {
          rc_t     rc   = kOkRC;

          // save the current starting message
          unsigned beg_file_idx = p->beg_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->beg_msg_idx ].file_idx;
          unsigned beg_msg_idx  = p->beg_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->beg_msg_idx ].msg_idx;

          // if the 'beg' file does not exist
          if( beg_file_idx != kInvalidIdx && _file_exists(p->fileA[ beg_file_idx ])==false )
          {  
            beg_file_idx = kInvalidIdx;
            beg_msg_idx  = kInvalidIdx;
          }

          // save the current ending message
          unsigned end_file_idx = p->end_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->end_msg_idx ].file_idx;
          unsigned end_msg_idx  = p->end_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->end_msg_idx ].msg_idx;

          // if the 'end' file does not exist
          if( end_file_idx != kInvalidIdx && _file_exists(p->fileA[ end_file_idx ])==false )
          {  
            end_file_idx = kInvalidIdx;
            end_msg_idx  = kInvalidIdx;
          }

          
          // calc the count of message in all the files
          p->msgAllocN = _calc_msg_count(p);

          // allocate a single array to hold the messages from all the files
          p->msgA = mem::resize<file_msg_t>(p->msgA,p->msgAllocN);

          // fill p->msgA[] from each of the files
          _fill_msg_array(p);

          // sort p->msgA[] on msgA[].amicro
          auto f = [](const file_msg_t& a0,const file_msg_t& a1) -> bool { return a0.amicro < a1.amicro; };
          std::sort(p->msgA,p->msgA+p->msgN,f);

          // by default we rewind to the first msg
          p->next_wr_msg_idx = 0;
          p->next_rd_msg_idx = 0;

          // if a valid seek position exists then reset it
          if( beg_file_idx != kInvalidIdx && beg_msg_idx != kInvalidIdx )
            if((rc = _seek_to_msg_index( p, beg_file_idx, beg_msg_idx )) != kOkRC )
              rc = cwLogError(rc,"The MIDI file device starting output message could not be restored.");

          if( end_file_idx != kInvalidIdx && end_msg_idx != kInvalidIdx )
            if((rc = _set_end_msg_index(p, end_file_idx, end_msg_idx )) != kOkRC )
              rc = cwLogError(rc,"The MIDI file device ending output message could not be restored.");
              
        
          return rc;        
        }

        void _update_active_flag( file_dev_t* p )
        {
            unsigned i=0;
            for(; i<p->fileN; ++i)
              if( _file_exists(p->fileA[i]) && p->fileA[i].enable_fl )
                break;
            
            p->is_activeFl = i < p->fileN;          
        }
      
        rc_t _enable_file(  handle_t h, unsigned file_idx, bool enable_fl )
        {
          rc_t rc;
  
          file_dev_t* p = _handleToPtr(h);
  
          if((rc = _validate_file_existence(p, file_idx)) != kOkRC )
            goto errLabel;

          p->fileA[ file_idx ].enable_fl = enable_fl;

          _update_active_flag(p);
          
        errLabel:

          if(rc != kOkRC )
            rc = cwLogError(rc,"MIDI file device %s failed on file index %i.", enable_fl ? "enable" : "disable", file_idx );
        
          return rc;
        }

        void _callback( file_dev_t* p, unsigned file_idx, msg_t* msgA, unsigned msgN )
        {
          if( p->cbFunc != nullptr )
          {
            packet_t pkt = {};
            pkt.devIdx   = p->base_dev_idx;
            pkt.portIdx  = file_idx;
            pkt.msgArray = msgA;
            pkt.msgCnt   = msgN;
            
            p->cbFunc( p->cbArg, &pkt, 1 );
          }
        }

        void _packetize_and_transmit_msgs( file_dev_t* p, unsigned xmt_msg_cnt  )
        {
          msg_t msgA[ xmt_msg_cnt ];
          
          unsigned pkt_msg_idx = 0;
          unsigned file_idx_0  = kInvalidIdx;

          assert( p->next_rd_msg_idx != kInvalidIdx && p->next_wr_msg_idx != kInvalidIdx );
          
          for(; p->next_rd_msg_idx < p->next_wr_msg_idx && pkt_msg_idx < xmt_msg_cnt; ++p->next_rd_msg_idx)            
          {
            const file_msg_t& m = p->msgA[ p->next_rd_msg_idx ];
            
            if( p->fileA[ m.file_idx ].enable_fl && m.msg != nullptr  )
            {
              // 
              if( p->latency_meas_enable_in_fl && isNoteOn(m.msg->status,m.msg->d1) )
              {
                p->latency_meas_enable_in_fl = false;
                time::get(p->latency_meas_result.note_on_input_ts);
              }
              
              // if the file_idx is not the same as the previous messages then
              // send the currently stored messages from msgA[] - because packet
              // messages must all belong to the same port
              if( file_idx_0 != kInvalidIdx && m.file_idx != file_idx_0 )
              {
                _callback(p,file_idx_0,msgA,pkt_msg_idx);
                pkt_msg_idx = 0;
              }
              
              msgA[pkt_msg_idx]  = *m.msg;
              file_idx_0         = m.file_idx;
              pkt_msg_idx       += 1;
            }
          }
          
          if( pkt_msg_idx > 0 )
            _callback(p,file_idx_0,msgA,pkt_msg_idx);
        }

        rc_t _load_messages( handle_t h, unsigned file_idx, const char* fname, const msg_t* msgA, unsigned msgN )
        {
          rc_t rc;
          file_dev_t* p = _handleToPtr(h);

          if((rc = _validate_file_index(p,file_idx)) != kOkRC )
            goto errLabel;

          if((rc = _close_file(p,file_idx)) != kOkRC )
            goto errLabel;

          if( fname != nullptr )
          {
            if((rc = _open_midi_file(p,file_idx,fname)) != kOkRC )
              goto errLabel;            
          }
          else
          {
            if( msgA != nullptr )
            {
              p->fileA[ file_idx ].msgA = mem::allocZ<msg_t>(msgN);
              p->fileA[ file_idx ].msgN = msgN;
              memcpy(p->fileA[ file_idx ].msgA, msgA, msgN*sizeof(msg_t));
            }
            else
            {
              assert(0);
            }
          }
          
          if((rc = _prepare_msg_array(p)) != kOkRC )
            goto errLabel;

          p->fileA[ file_idx ].enable_fl = true;
  
        errLabel:

          _update_active_flag(p);
  
          if( rc != kOkRC )
            rc = cwLogError(rc,"MIDI file device msg. load port failed %i.",file_idx);
  
          return rc;  
        }
        
      }
    }
  } 
}


cw::rc_t cw::midi::device::file_dev::create( handle_t&   hRef,
                                             cbFunc_t    cbFunc,
                                             void*       cbArg,
                                             unsigned    baseDevIdx,
                                             const char* labelA[],
                                             unsigned    max_file_cnt,
                                             const char* dev_name,
                                             unsigned    read_ahead_micros)
{
  rc_t rc;
  
  if((rc = destroy(hRef)) != kOkRC )
    return rc;

  file_dev_t* p = mem::allocZ<file_dev_t>();

  p->cbFunc                   = cbFunc;
  p->cbArg                    = cbArg;
  p->fileN                    = max_file_cnt;
  p->fileA                    = mem::allocZ<file_t>(p->fileN);
  p->devCnt                   = 1;
  p->is_activeFl              = false;
  p->read_ahead_micros        = read_ahead_micros;
  p->base_dev_idx             = baseDevIdx;
  p->dev_name                 = mem::duplStr(dev_name);

  _reset_indexes(p);
  
  for(unsigned i=0; i<p->fileN; ++i)
  {
    if( labelA[i] != nullptr )
    {
      p->fileA[i].label = mem::duplStr(labelA[i]);
    }
    else
    {
      rc = cwLogError(kInvalidArgRC,"Count of MIDI file device labels must match the max file count.");
      goto errLabel;
    }
  }

  hRef.set(p);

errLabel:
  if(rc != kOkRC )
    _destroy(p);
  return rc;
}

cw::rc_t cw::midi::device::file_dev::destroy( handle_t& hRef )
{
  rc_t rc = kOkRC;
  if(!hRef.isValid() )
    return rc;

  file_dev_t* p = _handleToPtr(hRef);

  if((rc = _destroy(p)) != kOkRC )
  {
    rc = cwLogError(rc,"MIDI file device destroy failed.");
    goto errLabel;
  }

  hRef.clear();

errLabel:
  return rc;
}

bool cw::midi::device::file_dev::is_active( handle_t h )
{
  file_dev_t* p = _handleToPtr(h);  
  return p->is_activeFl;
}

unsigned cw::midi::device::file_dev::file_count( handle_t h )
{
  file_dev_t* p = _handleToPtr(h);
  return p->fileN;
}

cw::rc_t cw::midi::device::file_dev::open_midi_file( handle_t h, unsigned file_idx, const char* fname )
{
  return _load_messages(h,file_idx,fname,nullptr,0);
}

cw::rc_t cw::midi::device::file_dev::load_messages( handle_t h, unsigned file_idx, const msg_t* msgA, unsigned msgN )
{
  return _load_messages(h,file_idx,nullptr,msgA,msgN);
}


cw::rc_t cw::midi::device::file_dev::enable_file(  handle_t h, unsigned file_idx, bool enableFl )
{ return _enable_file(h,file_idx,enableFl); }

cw::rc_t cw::midi::device::file_dev::enable_file(  handle_t h, unsigned file_idx )
{ return _enable_file(h,file_idx,true); }

cw::rc_t cw::midi::device::file_dev::disable_file( handle_t h,unsigned file_idx )
{ return _enable_file(h,file_idx,false); }


unsigned    cw::midi::device::file_dev::count( handle_t h )
{
  file_dev_t* p = _handleToPtr(h);
  return p->devCnt;  
}

const char* cw::midi::device::file_dev::name( handle_t h, unsigned devIdx )
{
  file_dev_t* p = _handleToPtr(h);

  return _validate_dev_index(p,devIdx)==kOkRC ? p->dev_name : nullptr;
}

unsigned    cw::midi::device::file_dev::nameToIndex(handle_t h, const char* deviceName)
{
  file_dev_t* p = _handleToPtr(h);  
  return textIsEqual(deviceName,p->dev_name) ? 0 : kInvalidIdx;    
}

unsigned    cw::midi::device::file_dev::portCount(  handle_t h, unsigned devIdx, unsigned flags )
{
  file_dev_t* p = _handleToPtr(h);

  if(_validate_dev_index(p,devIdx) != kOkRC )
    return 0;
  
  return flags & kInMpFl ? p->fileN : 0;
}

const char* cw::midi::device::file_dev::portName(   handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx )
{
  file_dev_t* p = _handleToPtr(h);

  if( _validate_dev_index(p,devIdx) != kOkRC )
    return nullptr;

  if( _validate_port_index(p,portIdx) != kOkRC )
    return nullptr;

  return p->fileA[portIdx].label;
}

unsigned    cw::midi::device::file_dev::portNameToIndex( handle_t h, unsigned devIdx, unsigned flags, const char* portName )
{
  file_dev_t* p = _handleToPtr(h);

  if( _validate_dev_index(p,devIdx) != kOkRC )
    return kInvalidIdx;

  if( flags & kInMpFl )
  {
    for(unsigned i=0; i<p->fileN; ++i)
      if( textIsEqual(p->fileA[i].label,portName) )
        return i;
  }
  return kInvalidIdx;  
}

cw::rc_t  cw::midi::device::file_dev::portEnable( handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx, bool enableFl )
{
  rc_t        rc = kOkRC;
  file_dev_t* p = _handleToPtr(h);
  
  if((rc = _validate_dev_index(p,devIdx)) != kOkRC )
    goto errLabel;
  
  if( flags & kInMpFl )
  {
    rc = enable_file(h,portIdx,enableFl);
  }
  else
  {
    rc = cwLogError(kNotImplementedRC,"MIDI file dev output enable/disable has not been implemented.");
  }

    
errLabel:
  if(rc != kOkRC )
    rc = cwLogError(rc,"MIDI file dev port enable/disable failed.");
  
  return rc;
}

unsigned cw::midi::device::file_dev::msg_count( handle_t h, unsigned file_idx )
{
  file_dev_t* p = _handleToPtr(h);
  rc_t rc;
  
  if((rc = _validate_file_existence(p,file_idx)) != kOkRC )
    goto errLabel;
  
  return p->msgN;
    
errLabel:
  return 0;
}

cw::rc_t cw::midi::device::file_dev::seek_to_msg_index( handle_t h, unsigned file_idx, unsigned msg_idx )
{
  file_dev_t* p = _handleToPtr(h);

  return _seek_to_msg_index(p,file_idx,msg_idx);
}

cw::rc_t cw::midi::device::file_dev::set_end_msg_index( handle_t h, unsigned file_idx, unsigned msg_idx )
{
  file_dev_t* p = _handleToPtr(h);
  return _set_end_msg_index(p, file_idx, msg_idx );
}
  


cw::rc_t cw::midi::device::file_dev::rewind( handle_t h )
{
  file_dev_t* p = _handleToPtr(h);

  if( p->beg_msg_idx == kInvalidIdx )
  {
    p->next_wr_msg_idx = 0;
    p->next_rd_msg_idx = 0;    
  }
  else
  {
    p->next_wr_msg_idx = p->beg_msg_idx;
    p->next_rd_msg_idx = p->beg_msg_idx;
  }
  
  return kOkRC;
}

cw::rc_t cw::midi::device::file_dev::set_start_delay(  handle_t h, unsigned start_delay_micros )
{
  file_dev_t* p = _handleToPtr(h);

  p->start_delay_micros = start_delay_micros;

  return kOkRC;
  
}

cw::midi::device::file_dev::exec_result_t cw::midi::device::file_dev::exec( handle_t h, unsigned long long cur_time_us )
{
  exec_result_t r;
  
  file_dev_t*p = _handleToPtr(h);

  // p->end_msg_idx indicates the last msg to transmit, but we wait until the msg following p->end_msg_idx to
  // actually stop transmitting and set r.eof_fl.  This will facilitate providing a natural time gap to loop to the beginning.
  unsigned end_msg_idx  = p->end_msg_idx == kInvalidIdx ? p->msgN-1 : p->end_msg_idx;

  
  // if there are no messages left to send
  if( p->next_wr_msg_idx==kInvalidIdx || p->next_wr_msg_idx >= p->msgN || p->next_wr_msg_idx > p->end_msg_idx )
  {
    r.next_msg_wait_micros = 0;
    r.xmit_cnt = 0;
    r.eof_fl = true;
  }
  else
  {

    if( cur_time_us < p->start_delay_micros )
    {
      r.xmit_cnt = 0;
      r.eof_fl   = false;
      r.next_msg_wait_micros = p->start_delay_micros - cur_time_us;
    }
    else
    {
      cur_time_us -= p->start_delay_micros;
      
      unsigned            base_msg_idx = p->beg_msg_idx == kInvalidIdx ? 0         : p->beg_msg_idx;
      
      assert( base_msg_idx <= p->next_wr_msg_idx );
      
      unsigned long long  msg_time_us  = p->msgA[ p->next_wr_msg_idx ].amicro - p->msgA[ base_msg_idx ].amicro;
      unsigned            xmit_cnt     = 0;
      unsigned long long  end_time_us  = cur_time_us + p->read_ahead_micros;
      
      // for all msgs <= current time + read_ahead_micros
      while( msg_time_us <= end_time_us )
      {
        //
        // consume msg here
        //

        xmit_cnt += 1;
      
        // advance to next msg
        p->next_wr_msg_idx += 1; 
            
        // check for EOF
        if( p->next_wr_msg_idx >= p->msgN)
          break;

        // time of next msg
        msg_time_us = p->msgA[ p->next_wr_msg_idx ].amicro - p->msgA[ base_msg_idx ].amicro;

        // if we went past the end msg then stop
        if( p->next_wr_msg_idx > end_msg_idx )
          break;
      }

      assert( p->next_wr_msg_idx >= p->msgN || msg_time_us > cur_time_us );
    
      r.xmit_cnt             = xmit_cnt;
      r.eof_fl               = p->next_wr_msg_idx >= p->msgN;
      r.next_msg_wait_micros = r.eof_fl ? 0 : msg_time_us - cur_time_us;
    
      // callback with output msg's
      if( xmit_cnt )
        _packetize_and_transmit_msgs( p, xmit_cnt  );
    }
    
  }

  return r;
          
}


cw::rc_t    cw::midi::device::file_dev::send( handle_t h, unsigned devIdx, unsigned portIdx, uint8_t st, uint8_t d0, uint8_t d1 )
{
  return cwLogError(kNotImplementedRC,"MIDI file dev send() not implemented.");

  file_dev_t* p  = _handleToPtr(h);
  
  if( p->latency_meas_enable_out_fl && isNoteOn(st,d1) )
  {
    p->latency_meas_enable_out_fl = false;
    time::get(p->latency_meas_result.note_on_output_ts);
  }
  
}

cw::rc_t    cw::midi::device::file_dev::sendData( handle_t h, unsigned devIdx, unsigned portIdx, const uint8_t* dataPtr, unsigned byteCnt )
{
  return cwLogError(kNotImplementedRC,"MIDI file dev send() not implemented."); 
}

void cw::midi::device::file_dev::latency_measure_reset(handle_t h)
{
  file_dev_t* p  = _handleToPtr(h);
  
  p->latency_meas_result.note_on_input_ts = {};
  p->latency_meas_result.note_on_output_ts = {};
  p->latency_meas_enable_in_fl           = true;
  p->latency_meas_enable_out_fl          = true;
}

cw::midi::device::latency_meas_result_t cw::midi::device::file_dev::latency_measure_result(handle_t h)
{
  file_dev_t* p  = _handleToPtr(h);
  return p->latency_meas_result;
}


void cw::midi::device::file_dev::report( handle_t h, textBuf::handle_t tbH)
{
  file_dev_t* p = _handleToPtr(h);

  print(tbH,"%i : Device: '%s'\n",p->base_dev_idx,p->dev_name);
  
  if( p->fileN )
    print(tbH,"  Input:\n");
  
  
  for(unsigned i=0; i<p->fileN; ++i)
  {
    const char* fname = "<none>";
    
    if( p->fileA[i].fname != nullptr )
    {
      fname = p->fileA[i].fname;
    }
    else
      if( p->fileA[i].msgA != nullptr )
      {
        fname = "msg array";
      }
    
    print(tbH,"               port:%i    '%s' ena:%i msg count:%i %s\n",
          i,
          cwStringNullGuard(p->fileA[i].label),
          p->fileA[i].enable_fl,
          p->fileA[i].msgN,
          fname );
  }
  
}


