#include "cmGlobal.h"
#include "cmFloatTypes.h"
#include "cmRpt.h"
#include "cmErr.h"
#include "cmCtx.h"
#include "cmMem.h"
#include "cmMallocDebug.h"
#include "cmAudioFile.h"
#include "cmThread.h"
#include "cmTime.h"
#include "cmAudioPort.h"
#include "cmAudioFileDev.h"
#include "cmTime.h"

cmAfdH_t cmAfdNullHandle = cmSTATIC_NULL_HANDLE;

#define cmAfd_Billion (1000000000)
#define cmAfd_Million    (1000000)

typedef struct
{
  cmErr_t           err;             // error object 
  cmApCallbackPtr_t callbackPtr;     // client callback function
  void*             cbDataPtr;       // argument to be passed with the client callback 

  unsigned          devIdx;
  cmChar_t*         label;
  cmChar_t*         oFn; 
  unsigned          oBits;
  unsigned          oChCnt;

  cmAudioFileH_t    iAfH;            // audio input file handle
  cmAudioFileH_t    oAfH;            // audio output file handle
  cmThreadH_t       tH;              // thread handle

  double            srate;           // file device sample rate
  unsigned          framesPerCycle;  // count of samples sent/recv'd from the client on each callback

  cmApAudioPacket_t iPkt;            // audio packet used sent to the client via callbackPtr.
  cmApAudioPacket_t oPkt;            //
  cmApSample_t**    iChArray;        // audio buffer channel arrays used with cmAudioFile
  cmApSample_t**    oChArray;        //

  bool              runFl;            // set to true as long as the thread should continue looping
  bool              rewindFl;         // set to true when the input file should rewind
  unsigned          readErrCnt;       // count of read errors from the input file
  bool              eofFl;            // set to true when the input file reaches the EOF
  unsigned          writeErrCnt;      // count of write errors from the output file

  long              nanosPerCycle;    // nano-seconds per cycle
  struct timespec   baseTime;      
  struct timespec   nextTime;         // next execution time 

  unsigned          cycleCnt;         // count of cycles completed

} cmAfd_t;

cmAfd_t* _cmAfdHandleToPtr( cmAfdH_t h )
{
  cmAfd_t* p = (cmAfd_t*)h.h;
  assert(p != NULL );
  return p;
}

// 
void _cmAudioFileDevExec( cmAfd_t* p )
{
  unsigned iPktCnt = 0;
  unsigned oPktCnt = p->oPkt.chCnt!=0;

  // if the input device is enabled
  if( p->iPkt.chCnt )
  {
    unsigned actualFrmCnt = p->framesPerCycle;

    // if the input file has reached EOF - zero the input buffer
    if( p->eofFl )
      memset(p->iPkt.audioBytesPtr,0,p->framesPerCycle*sizeof(cmApSample_t));
    else
    {
      // otherwise fill the input buffer from the input file
      if( cmAudioFileReadSample(p->iAfH, p->framesPerCycle, p->iPkt.begChIdx, p->iPkt.chCnt, p->iChArray, &actualFrmCnt) != kOkAfRC )
        ++p->readErrCnt;

      // if the input file reachged EOF the set p->eofFl
      if( (actualFrmCnt < p->framesPerCycle) && cmAudioFileIsEOF(p->iAfH) )
        p->eofFl = true;
    }

    iPktCnt = actualFrmCnt>0;
  }

  // callback to the client to provde incoming samples and receive outgoing samples
  p->callbackPtr(iPktCnt ? &p->iPkt : NULL, iPktCnt, oPktCnt ? &p->oPkt : NULL, oPktCnt );

  // if the output device is enabled
  if( p->oPkt.chCnt )
  {
    // write the output samples
    if( cmAudioFileWriteSample( p->oAfH, p->framesPerCycle, p->oPkt.chCnt, p->oChArray ) != kOkAfRC )
      ++p->writeErrCnt;
  }

  ++p->cycleCnt;

}

// incrment p->nextTime to the next execution time
void _cmAfdIncrNextTime( cmAfd_t* p )
{
  long nsec            = p->nextTime.tv_nsec + p->nanosPerCycle;

  if( nsec < cmAfd_Billion )
    p->nextTime.tv_nsec = nsec;
  else
  {
    p->nextTime.tv_sec  += 1;
    p->nextTime.tv_nsec  = nsec - cmAfd_Billion;
  }
}

// calc the time between t1 and t0 - t1 is assummed to come after t0 in order to produce a positive result
long  _cmAfdDiffMicros( const struct timespec* t0, const struct timespec* t1 )
{
  long u0 = t0->tv_sec * cmAfd_Million;
  long u1 = t1->tv_sec * cmAfd_Million;
  
  u0 += t0->tv_nsec / 1000;
  u1 += t1->tv_nsec / 1000;

  return u1 - u0;
}

//  thread callback function 
bool _cmAudioDevThreadFunc(void* param)
{
  cmAfd_t* p       = (cmAfd_t*)param;
  struct timespec t0;

  // if this is the first time this callback has been called after a call to cmAudioFileDevStart().
  if( p->cycleCnt == 0 )
  {
    // get the baseTime - all other times will be relative to this time
    cmTimeGet(&p->baseTime);
    p->nextTime = p->baseTime;
    p->nextTime.tv_sec = 0;
    _cmAfdIncrNextTime(p);
  }

  // if the thread has not been requested to stop
  if( p->runFl )
  {
    // get the current time as an offset from baseTime.
    cmTimeGet(&t0);
    t0.tv_sec -= p->baseTime.tv_sec;

    // get length of time to next exec point
    long dusec = _cmAfdDiffMicros(&t0, &p->nextTime);

    // if the execution time has not yet arrived
    if( dusec > 0 )
    {
      cmSleepUs(dusec);
    }

    // if the thread is still running
    if( p->runFl )
    {
      // read/callback/write
      _cmAudioFileDevExec(p);
  
      // calc the next exec time
      _cmAfdIncrNextTime(p);
    }
  }

  return p->runFl;
}

cmAfdRC_t cmAudioFileDevInitialize( 
  cmAfdH_t*       hp, 
  const cmChar_t* label,
  unsigned        devIdx,
  const cmChar_t* iFn,
  const cmChar_t* oFn,
  unsigned        oBits,
  unsigned        oChCnt,
  cmRpt_t*        rpt )
{
  cmAfdRC_t rc;
  cmRC_t    afRC;

  if((rc = cmAudioFileDevFinalize(hp)) != kOkAfdRC )
    return rc;
  
  // allocate the object
  cmAfd_t* p = cmMemAllocZ(cmAfd_t,1);

  hp->h = p;

  cmErrSetup(&p->err,rpt,"AudioFileDevice");

  // create the input audio file handle
  if( iFn != NULL )
  {
    cmAudioFileInfo_t afInfo;

    // open the input file
    if(cmAudioFileIsValid( p->iAfH = cmAudioFileNewOpen(iFn,&afInfo,&afRC,rpt)) == false )
    {
      rc = cmErrMsg(&p->err,kAudioFileFailAfdRC,"The audio input file '%s' could not be opened.", iFn);
      goto errLabel;
    }

    p->iPkt.devIdx         = devIdx;
    p->iPkt.begChIdx       = 0;
    p->iPkt.chCnt          = afInfo.chCnt;   // setting iPkt.chCnt to a non-zero value marks the input file as active
    p->iPkt.audioFramesCnt = 0;
    p->iPkt.bitsPerSample  = afInfo.bits;
    p->iPkt.flags          = kFloatApFl;
    p->iPkt.audioBytesPtr  = NULL;
    p->iChArray            = cmMemResizeZ( cmApSample_t*, p->iChArray, afInfo.chCnt );
    p->readErrCnt          = 0;
    p->eofFl               = false;
  }

  // create the output audio file handle
  if(cmAudioFileIsValid( p->oAfH = cmAudioFileNewOpen(NULL,NULL,NULL,rpt)) == false )
  {
    rc = cmErrMsg(&p->err,kAudioFileFailAfdRC,"The audio output file object allocation failed.");
    goto errLabel;
  }

  // create the driver thread
  if( cmThreadCreate(&p->tH, _cmAudioDevThreadFunc, p, rpt ) != kOkThRC )
  {
    rc = cmErrMsg(&p->err,kThreadFailAfdRC,"The internal thread could not be created.");
    goto errLabel;
  }

  p->runFl  = true;
  p->devIdx = devIdx;
  p->label  = cmMemAllocStr(label);
  p->oFn    = cmMemAllocStr(oFn);
  p->oBits  = oBits;
  p->oChCnt = oChCnt;

 errLabel:
  if( rc != kOkAfdRC )
    cmAudioFileDevFinalize(hp);

  return rc;
}

cmAfdRC_t cmAudioFileDevFinalize( cmAfdH_t* hp )
{
  if( hp == NULL || cmAudioFileDevIsValid(*hp) == false )
    return kOkAfdRC;

  cmAfd_t* p = _cmAfdHandleToPtr(*hp);

  p->runFl = false;

  if( cmThreadIsValid(p->tH) )
    cmThreadDestroy(&p->tH);

  cmAudioFileDelete(&p->iAfH);
  cmAudioFileDelete(&p->oAfH);
  cmMemPtrFree(&p->label);
  cmMemPtrFree(&p->oFn);
  cmMemPtrFree(&p->iPkt.audioBytesPtr);
  cmMemPtrFree(&p->oPkt.audioBytesPtr);
  cmMemPtrFree(&p->iChArray);
  cmMemPtrFree(&p->oChArray);
  cmMemPtrFree(&p);
  hp->h = NULL;

  return kOkAfdRC;
}

bool      cmAudioFileDevIsValid( cmAfdH_t h )
{ return h.h != NULL;  }


cmAfdRC_t cmAudioFileDevSetup( 
  cmAfdH_t                        h, 
  unsigned                        baseDevIdx,
  double                          srate, 
  unsigned                        framesPerCycle, 
  cmApCallbackPtr_t               callbackPtr, 
  void*                           cbDataPtr )
{
  cmAfdRC_t rc        = kOkAfdRC;
  bool      restartFl = false;
  unsigned  i;

  if( cmAudioFileDevIsStarted(h) )
  {
    if((rc = cmAudioFileDevStop(h)) != kOkAfdRC )
      return rc;

    restartFl = true;
  }

  cmAfd_t* p = _cmAfdHandleToPtr(h);

  /*
  // close the existing input file
  if(cmAudioFileClose(&p->iAfH) != kOkAfRC )
    rc = cmErrMsg(&p->err,kAudioFileFailAfdRC,"Audio file close failed on input audio file.");

  p->iPkt.chCnt = 0; // mark the input file as inactive
  */

  if( cmAudioFileIsValid( p->iAfH ) )
    if( cmAudioFileSeek( p->iAfH, 0 ) != kOkAfRC )
      rc = cmErrMsg(&p->err,kAudioFileFailAfdRC,"Audio file device rewind failed.");


  // close the existing output file
  if(cmAudioFileClose(&p->oAfH) != kOkAfRC )
    rc = cmErrMsg(&p->err,kAudioFileFailAfdRC,"Audio file close failed on output audio file.");

  p->oPkt.chCnt = 0; // mark the output file as inactive

  // if an output audio file was given ...
  if( p->oFn != NULL )
  {
    // ... then open it
    if( cmAudioFileCreate( p->oAfH, p->oFn, srate, p->oBits, p->oChCnt ) != kOkAfRC )
    {
      rc = cmErrMsg(&p->err,kAudioFileFailAfdRC, "The audio output file '%s' could not be created.",p->oFn);
      goto errLabel;
    }

    cmApSample_t* bp = (cmApSample_t*)p->oPkt.audioBytesPtr;

    p->oPkt.devIdx         = p->devIdx + baseDevIdx;
    p->oPkt.begChIdx       = 0;
    p->oPkt.chCnt          = p->oChCnt;
    p->oPkt.audioFramesCnt = framesPerCycle;
    p->oPkt.bitsPerSample  = p->oBits;
    p->oPkt.flags          = kFloatApFl;
    p->oPkt.audioBytesPtr  = bp = cmMemResizeZ( cmApSample_t, bp, framesPerCycle*p->oChCnt );
    p->oPkt.userCbPtr      = cbDataPtr;
    p->oChArray            = cmMemResizeZ( cmApSample_t*, p->oChArray, p->oChCnt );
    
    for(i=0; i<p->oChCnt; ++i)
      p->oChArray[i] = bp + (i*framesPerCycle);

  }

  if( cmAudioFileIsValid( p->iAfH) )
  {
    cmApSample_t* bp = (cmApSample_t*)p->iPkt.audioBytesPtr;

    p->iPkt.devIdx         = p->devIdx + baseDevIdx;
    p->iPkt.audioFramesCnt = framesPerCycle;
    p->iPkt.audioBytesPtr  = bp = cmMemResizeZ( cmApSample_t, bp, framesPerCycle*p->iPkt.chCnt ); ;
    p->iPkt.userCbPtr      = cbDataPtr;
    for(i=0; i<p->iPkt.chCnt; ++i)
      p->iChArray[i] = bp + (i*framesPerCycle);
  }

  p->callbackPtr    = callbackPtr;
  p->cbDataPtr      = cbDataPtr;
  p->framesPerCycle = framesPerCycle;
  p->srate          = srate;
  p->cycleCnt       = 0;
  p->nanosPerCycle  = floor((double)framesPerCycle / srate * cmAfd_Billion );

  if( restartFl )
  {
    if((rc = cmAudioFileDevStart(h)) != kOkAfdRC )
    {
      rc = cmErrMsg(&p->err,kRestartFailAfdRC,"The audio file device could not be restarted.");
    }
  }

 errLabel:
  return rc;

}

const char* cmAudioFileDevLabel( cmAfdH_t h )
{
  cmAfd_t*  p  = _cmAfdHandleToPtr(h); 
  return p->label;
}

unsigned    cmAudioFileDevChannelCount( cmAfdH_t h, bool inputFl )
{
  cmAfd_t*  p  = _cmAfdHandleToPtr(h); 
  return inputFl ? p->iPkt.chCnt : p->oPkt.chCnt;
}

double      cmAudioFileDevSampleRate(   cmAfdH_t h )
{
  cmAfd_t*  p  = _cmAfdHandleToPtr(h);
  return p->srate;
}

unsigned    cmAudioFileDevFramesPerCycle( cmAfdH_t h, bool inputFl )
{
  cmAfd_t*  p  = _cmAfdHandleToPtr(h);
  return inputFl ? p->iPkt.audioFramesCnt : p->oPkt.audioFramesCnt;
}


cmAfdRC_t cmAudioFileDevRewind( cmAfdH_t h )
{
  cmAfd_t*  p  = _cmAfdHandleToPtr(h);
  p->rewindFl = true;
  return kOkAfdRC;
}

cmAfdRC_t cmAudioFileDevStart( cmAfdH_t h )
{
  cmAfdRC_t rc = kOkAfdRC;
  cmAfd_t*  p  = _cmAfdHandleToPtr(h);

  p->cycleCnt = 0;

  if( cmThreadPause( p->tH, 0 ) != kOkThRC )
  {
    rc = cmErrMsg(&p->err,kThreadFailAfdRC,"Thread start failed.");
    goto errLabel;
  }

  fputs("Start\n",stderr);

 errLabel:
  return rc;
}

cmAfdRC_t cmAudioFileDevStop( cmAfdH_t h )
{
  cmAfdRC_t rc = kOkAfdRC;
  cmAfd_t*  p  = _cmAfdHandleToPtr(h);

  if( cmThreadPause( p->tH, kPauseThFl | kWaitThFl ) != kOkThRC )
  {
    rc = cmErrMsg(&p->err,kThreadFailAfdRC,"Thread stop failed.");
    goto errLabel;
  }

  fputs("Stop\n",stderr);

 errLabel:
  return rc;
}

bool      cmAudioFileDevIsStarted( cmAfdH_t h ) 
{  
  cmAfd_t* p = _cmAfdHandleToPtr(h);
  return cmThreadState(p->tH) == kRunningThId; 
}

void cmAudioFileDevReport( cmAfdH_t h, cmRpt_t* rpt )
{
  cmAfd_t* p = _cmAfdHandleToPtr(h);
  cmRptPrintf(rpt,"label:%s thr state:%i srate:%f\n",p->label,cmThreadState(p->tH),p->srate);
  cmRptPrintf(rpt, "in  chs:%i %s\n",p->iPkt.chCnt,cmAudioFileName(p->iAfH));
  cmRptPrintf(rpt, "out chs:%i %s\n",p->oPkt.chCnt,p->oFn);
}

// device callback function used with cmAudioFileDevTest() note that this assumes
// that the packet buffer contain non-interleaved data.
void _cmAfdCallback( 
  cmApAudioPacket_t* inPktArray, 
  unsigned           inPktCnt, 
  cmApAudioPacket_t* outPktArray, 
  unsigned           outPktCnt )
{
  cmApAudioPacket_t* ip  = inPktArray;
  cmApAudioPacket_t* op  = outPktArray;
  unsigned           opi = 0;
  unsigned           ipi = 0;
  unsigned           oci = 0;
  unsigned           ici = 0;

  while(1)
  {
    if( ici == ip->chCnt)
    {
      ici = 0;
      if( ++ipi >= inPktCnt )
        break;

      ip = inPktArray + ipi;
    }


    if( oci == op->chCnt )
    {
      oci = 0;
      if( ++opi >= outPktCnt )
        break;

      ip = outPktArray + opi;
    }

    assert( ip->audioFramesCnt == op->audioFramesCnt );
    assert( cmIsFlag(ip->flags,kInterleavedApFl)==false && cmIsFlag(ip->flags,kInterleavedApFl)==false );

    cmApSample_t* ibp = ((cmApSample_t*)ip->audioBytesPtr) + (ip->audioFramesCnt*ici);
    cmApSample_t* obp = ((cmApSample_t*)op->audioBytesPtr) + (op->audioFramesCnt*oci);

    memcpy(obp,ibp,ip->audioFramesCnt*sizeof(cmApSample_t));

    ++ici;
    ++oci;
  }
}

void      cmAudioFileDevTest( cmRpt_t* rpt )
{
  cmAfdH_t        afdH           = cmAfdNullHandle;
  double          srate          = 44100;
  unsigned        framesPerCycle = 512;
  void*           cbDataPtr      = NULL;
  unsigned        devIdx         = 0;
  const cmChar_t* iFn            = "/home/kevin/media/audio/McGill-1/1 Audio Track.aiff";
  const cmChar_t* oFn            = "/home/kevin/temp/afd0.aif";
  unsigned        oBits          = 16;
  unsigned        oChCnt         = 2;

  if( cmAudioFileDevInitialize(&afdH,"file",devIdx,iFn,oFn,oBits,oChCnt,rpt) != kOkAfdRC )
    goto errLabel;

  if( cmAudioFileDevSetup(afdH,0,srate,framesPerCycle,_cmAfdCallback,cbDataPtr) != kOkAfdRC )
    goto errLabel;

  char c;
  fputs("q=quit 1=start 0=stop\n",stderr);
  fflush(stderr);

  while((c=getchar()) != 'q')
  {
    switch(c)
    {
      case '1': cmAudioFileDevStart(afdH); break;
      case '0': cmAudioFileDevStop(afdH);  break;
    }
    
    c = 0;
    fflush(stdin);
  }

 errLabel:
  cmAudioFileDevFinalize(&afdH);

  
}