//| 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 "cwObject.h"
#include "cwFileSys.h"
#include "cwFile.h"
#include "cwTime.h"
#include "cwIo.h"
#include "cwIoAudioRecordPlay.h"
#include "cwAudioFile.h"

namespace cw
{
  namespace audio_record_play
  {
    typedef io::sample_t sample_t;
    
    typedef struct am_audio_str
    {
      time::spec_t         timestamp;
      unsigned             chCnt;
      unsigned             dspFrameCnt;
      struct am_audio_str* link;
      sample_t         audioBuf[]; // [[ch0:dspFramCnt][ch1:dspFrmCnt]] total: chCnt*dspFrameCnt samples
    } am_audio_t;

    typedef struct audio_record_play_str
    {
      io::handle_t       ioH;
      
      am_audio_t*    audioBeg;       // first in a chain of am_audio_t audio buffers
      am_audio_t*    audioEnd;       // last in a chain of am_audio_t audio buffers
        
      am_audio_t*    audioFile;      // one large audio buffer holding the last loaded audio file

      double         srate;
      unsigned       curFrameCnt;
      unsigned       curFrameIdx;
      bool           recordFl;
      bool           startedFl;

      bool           mute_fl;
      
      unsigned*      audioInChMapA;
      unsigned       audioInChMapN;
      unsigned*      audioOutChMapA;
      unsigned       audioOutChMapN;
      
    } audio_record_play_t;

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

    void _am_audio_free_list( audio_record_play_t* p )
    {
      for(am_audio_t* a=p->audioBeg; a!=nullptr; )
      {
        am_audio_t* tmp = a->link;
        mem::release(a);
        a = tmp;
      }

      if( p->audioFile == p->audioBeg )
        p->audioFile = nullptr;
      else
        mem::release(p->audioFile);
      
      p->audioBeg = nullptr;
      p->audioEnd = nullptr;
      p->curFrameIdx = 0;
      p->curFrameCnt = 0;
      
    }
    
    am_audio_t* _am_audio_alloc( unsigned dspFrameCnt, unsigned chCnt )
    {
      unsigned    sample_byte_cnt = chCnt * dspFrameCnt * sizeof(sample_t);        
      void*       vp              = mem::allocZ<uint8_t>( sizeof(am_audio_t) + sample_byte_cnt );
      am_audio_t* a               = (am_audio_t*)vp;
        
      a->chCnt       = chCnt;
      a->dspFrameCnt = dspFrameCnt;
        
      return a;
    }

    
    rc_t _destroy( audio_record_play_t* p )
    {
      _am_audio_free_list(p);
      p->audioInChMapN = 0;
      p->audioOutChMapN = 0;
      mem::release(p->audioInChMapA);
      mem::release(p->audioOutChMapA);
      mem::release(p->audioFile);
      mem::release(p);
      return kOkRC;
    }

    rc_t _parseCfg(audio_record_play_t* p, const object_t& cfg )
    {
      rc_t rc = kOkRC;

      const object_t* audioInChMapL  = nullptr;
      const object_t* audioOutChMapL = nullptr;

      if((rc = cfg.getv_opt("audio_in_ch_map",  audioInChMapL,
                            "audio_out_ch_map", audioOutChMapL)) != kOkRC )
      {
        rc = cwLogError(rc,"Parse cfg failed.");
        goto errLabel;          
      }

      if( audioInChMapL != nullptr )
      {
        p->audioInChMapN = audioInChMapL->child_count();
        p->audioInChMapA = mem::allocZ<unsigned>( p->audioInChMapN );

        for(unsigned i=0; i<p->audioInChMapN; ++i)
          audioInChMapL->child_ele(i)->value(p->audioInChMapA[i]);
      }


      if( audioOutChMapL != nullptr )
      {
        p->audioOutChMapN = audioOutChMapL->child_count();
        p->audioOutChMapA = mem::allocZ<unsigned>( p->audioOutChMapN );

        for(unsigned i=0; i<p->audioOutChMapN; ++i)
          audioOutChMapL->child_ele(i)->value(p->audioOutChMapA[i]);
      }
      

    errLabel:
      return rc;
    }


    am_audio_t* _am_audio_from_sample_index( audio_record_play_t* p, unsigned sample_idx, unsigned& sample_offs_ref )
    {
      unsigned    n = 0;
      am_audio_t* a = p->audioBeg;

      if( p->audioBeg == nullptr )
        return nullptr;
        
      for(; a!=nullptr; a=a->link)
      {
        // if sample index falls inside this buffer
        if( n <= sample_idx && sample_idx < n + a->dspFrameCnt ) 
        {
          sample_offs_ref = sample_idx - n; // store the offset into this buffer of 'sample_idx'
          return a;
        }

        n += a->dspFrameCnt;

      }
          
      return nullptr;
    }

    void _audio_record( audio_record_play_t* p, const io::audio_msg_t& asrc )
    {
      unsigned chCnt = p->audioInChMapN==0 ? asrc.iBufChCnt : p->audioInChMapN;
      am_audio_t* a  = _am_audio_alloc(asrc.dspFrameCnt,chCnt);

      chCnt = std::min( chCnt, asrc.iBufChCnt );
        
      for(unsigned chIdx=0; chIdx<chCnt; ++chIdx)
      {
        unsigned srcChIdx = p->audioInChMapA == nullptr ? chIdx : p->audioInChMapA[chIdx];

        if( srcChIdx >= asrc.iBufChCnt )
          cwLogError(kInvalidArgRC,"Invalid input channel map index:%i >= %i.",srcChIdx,asrc.iBufChCnt);
        else
          memcpy(a->audioBuf + chIdx*asrc.dspFrameCnt, asrc.iBufArray[ srcChIdx ], asrc.dspFrameCnt * sizeof(sample_t));
      }
      
      a->chCnt        = chCnt;
      a->dspFrameCnt  = asrc.dspFrameCnt;

      if( p->audioEnd != nullptr )
        p->audioEnd->link = a;   // link the new audio record to the end of the audio sample buffer chain
      p->audioEnd       = a;     // make the new audio record the last ele. of the chain

      // if this is the first ele of the chain
      if( p->audioBeg == nullptr )
      {
        p->audioBeg    = a;
        p->srate       = asrc.srate;
        p->curFrameIdx = 0;
        p->curFrameCnt = 0;
      }

      p->curFrameIdx += asrc.dspFrameCnt;
      p->curFrameCnt += asrc.dspFrameCnt;
      
    }

    void _audio_play( audio_record_play_t* p, io::audio_msg_t& adst )
    {
      unsigned adst_idx = 0;

      if( !p->mute_fl )
      {
        while(adst_idx < adst.dspFrameCnt)
        {
          am_audio_t* a;
          unsigned sample_offs = 0;
          if((a = _am_audio_from_sample_index(p, p->curFrameIdx, sample_offs )) == nullptr )
            break;

          unsigned n  = std::min(a->dspFrameCnt - sample_offs, adst.dspFrameCnt );


          // TODO: Verify that this is correct - it looks like sample_offs should have to be incremented
        
          for(unsigned i=0; i<a->chCnt; ++i)
          {
            unsigned dstChIdx = p->audioOutChMapA != nullptr && i < p->audioOutChMapN ? p->audioOutChMapA[i] : i;

            if( dstChIdx < adst.oBufChCnt )
              memcpy( adst.oBufArray[ dstChIdx ] + adst_idx, a->audioBuf + sample_offs, n * sizeof(sample_t));
          }

          p->curFrameIdx += n;
          adst_idx       += n;
        }
      }
      
      // TODO: zero unused channels

      if( adst_idx < adst.dspFrameCnt )
        for(unsigned i=0; i<adst.oBufChCnt; ++i)
          memset( adst.oBufArray[i] + adst_idx, 0, (adst.dspFrameCnt - adst_idx) * sizeof(sample_t));
    }
    
    void _audio_through( audio_record_play_t* p, io::audio_msg_t& m )
    {
      unsigned chN     = std::min(m.iBufChCnt,m.oBufChCnt);
      unsigned byteCnt = m.dspFrameCnt * sizeof(sample_t);

      // Copy the input to the output
      for(unsigned i=0; i<chN; ++i)
        if( m.oBufArray[i] != NULL )
        {      
          // the input channel is not disabled
          if( m.iBufArray[i] != NULL )
          {
            for(unsigned j=0; j<m.dspFrameCnt; ++j )
              m.oBufArray[i][j] =  m.iBufArray[i][j];
          }
          else
          {
            // the input channel is disabled but the output is not - so fill the output with zeros
            memset(m.oBufArray[i], 0, byteCnt);
          }
        }
    }
      
    rc_t _audio_write_as_wav( audio_record_play_t* p, const char* fn )
    {
      rc_t                rc       = kOkRC;
      unsigned            frameCnt = 0;
      audiofile::handle_t afH;

      // if there is no audio to write
      if( p->audioBeg == nullptr )
        return rc;

      // create an audio file
      if((rc = audiofile::create( afH, fn, p->srate, 0, p->audioBeg->chCnt )) != kOkRC )
      {
        cwLogError(rc,"Audio file create failed.");
        goto errLabel;
      }

      // write each buffer 
      for(am_audio_t* a=p->audioBeg; a!=nullptr; a=a->link)
      {
        float* chBufArray[ a->chCnt ];
        for(unsigned i=0; i<a->chCnt; ++i)
          chBufArray[i] = a->audioBuf + (i*a->dspFrameCnt);
            
        if((rc = writeFloat( afH, a->dspFrameCnt, a->chCnt, chBufArray )) != kOkRC )
        {
          cwLogError(rc,"An error occurred while writing and audio buffer.");
          goto errLabel;
        }

        frameCnt += a->dspFrameCnt;
        
      }
        
    errLabel:

      // close the audio file
      if((rc = audiofile::close(afH)) != kOkRC )
      {
        cwLogError(rc,"Audio file close failed.");
        goto errLabel;
      }

      double secs = p->srate==0 ? 0 : (double)frameCnt/p->srate;

      cwLogInfo("Saved %f seconds of audio to %s.", secs, fn);
        
      return rc;
    }

    rc_t _audio_write_buffer_times( audio_record_play_t* p, const char* fn )
    {
      rc_t           rc = kOkRC;
      am_audio_t*    a0 = p->audioBeg;
      file::handle_t fH;
        
      // if there is no audio to write
      if( p->audioBeg == nullptr )
        return rc;

      // create the file
      if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC )
      {
        cwLogError(rc,"Create audio buffer time file failed.");
        goto errLabel;
      }

      file::print(fH,"{ [\n");
        
      // write each buffer
      for(am_audio_t* a=p->audioBeg; a!=nullptr; a=a->link)
      {
        unsigned elapsed_us = time::elapsedMicros( a0->timestamp, a->timestamp );
        file::printf(fH,"{ elapsed_us:%i chCnt:%i frameCnt:%i }\n", elapsed_us, a->chCnt, a->dspFrameCnt );
        a0 = a;
      }

      file::print(fH,"] }\n");

      // close the file
      if((rc = file::close(fH)) != kOkRC )
      {
        cwLogError(rc,"Close the audio buffer time file.");
        goto errLabel;
      }

    errLabel:
        
      return rc;        
    }

    rc_t _audio_read( audio_record_play_t* p, const char* fn )
    {
      rc_t                 rc = kOkRC;
      audiofile::handle_t  afH;
      audiofile::info_t    af_info;        
      am_audio_t*          am_audioFile;
      
      if((rc = audiofile::open(afH, fn, &af_info)) != kOkRC )
      {
        rc = cwLogError(kOpFailRC,"Audio file '%s' open failed.",fn);
        goto errLabel;         
      }
        
      if((am_audioFile = _am_audio_alloc(af_info.frameCnt,af_info.chCnt)) == nullptr )
      {
        rc = cwLogError(kOpFailRC,"Allocate audio buffer (%i samples) failed.",af_info.frameCnt*af_info.chCnt);
        goto errLabel;
      }
      else
      {
        unsigned audioFrameCnt = 0;
        float* chArray[ af_info.chCnt ];
          
        for(unsigned i=0; i<af_info.chCnt; ++i)
          chArray[i] = am_audioFile->audioBuf + (i*af_info.frameCnt);
          
        if((rc = audiofile::readFloat(afH, af_info.frameCnt, 0, af_info.chCnt, chArray, &audioFrameCnt)) != kOkRC )
        {
          rc = cwLogError(kOpFailRC,"Audio file read failed.");
          goto errLabel;
        }

        
        _am_audio_free_list(p);
        
        p->audioFile   = am_audioFile;
        p->audioBeg    = am_audioFile;
        p->audioEnd    = am_audioFile;
        p->srate       = af_info.srate;
        p->curFrameCnt = af_info.frameCnt;
        p->curFrameIdx = 0;

        double secs = p->srate==0 ? 0 : p->curFrameCnt / p->srate;
        cwLogInfo("Audio loaded: srate:%f secs:%f file:%s", p->srate, secs, cwStringNullGuard(fn));
        
      }
        
    errLabel:
      if((rc = audiofile::close(afH)) != kOkRC )
      {
        rc = cwLogError(kOpFailRC,"Audio file close failed.");
        goto errLabel;
      }
        
      return rc;
    }      

    rc_t _audio_callback( audio_record_play_t* p, io::audio_msg_t& m )
    {
      
      rc_t rc = kOkRC;

      if( p->startedFl )
      {
        if( p->recordFl )
        {
          if( m.iBufChCnt > 0 )
            _audio_record(p,m);            
        }
        else
        {
          if( m.oBufChCnt > 0 )
          {
            _audio_play(p,m);
          }
        }
      }
      else
      {
        for(unsigned i=0; i<m.oBufChCnt; ++i)
          memset( m.oBufArray[i], 0, m.dspFrameCnt * sizeof(sample_t)); 
      }
      
      return rc;
    }
  }
}



cw::rc_t cw::audio_record_play::create( handle_t& hRef, io::handle_t ioH, const object_t& cfg )
{
  rc_t rc;
  if((rc = destroy(hRef)) != kOkRC )
    return rc;

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

  if((rc = _parseCfg(p,cfg)) != kOkRC )
    return rc;
  
  p->ioH = ioH;

  hRef.set(p);
  
  return rc;
}

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

  audio_record_play_t* p = _handleToPtr(hRef);
  
  if((rc = _destroy(p)) != kOkRC )
    return rc;

  hRef.clear();

  return rc;
}
   
cw::rc_t cw::audio_record_play::start( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  
  p->curFrameIdx = 0;
  p->startedFl   = true;
  
  if( p->recordFl )
    _am_audio_free_list(p);
  
  return rc;
}

cw::rc_t cw::audio_record_play::stop( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  p->startedFl = false;
  return rc;
}

bool cw::audio_record_play::is_started( handle_t h )
{
  audio_record_play_t* p  = _handleToPtr(h);
  return p->startedFl;
}


cw::rc_t cw::audio_record_play::rewind( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  p->curFrameIdx = 0;
  return rc;
}

cw::rc_t cw::audio_record_play::clear( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  _am_audio_free_list(p);
  return rc;
}

cw::rc_t cw::audio_record_play::set_record_state( handle_t h, bool record_fl )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  p->recordFl = true;
  return rc;
}

bool cw::audio_record_play::record_state( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  return p->recordFl;
  return rc;
}

cw::rc_t cw::audio_record_play::set_mute_state( handle_t h, bool mute_fl )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  p->mute_fl = true;
  return rc;
}

bool cw::audio_record_play::mute_state( handle_t h )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  return p->mute_fl;
  return rc;
}

cw::rc_t cw::audio_record_play::save( handle_t h, const char* fn )
{
  audio_record_play_t* p  = _handleToPtr(h);
  return _audio_write_as_wav(p,fn);
}

cw::rc_t cw::audio_record_play::open( handle_t h, const char* fn )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  _audio_read(p,fn);
  return rc;
}

double cw::audio_record_play::duration_seconds( handle_t h )
{
  audio_record_play_t* p  = _handleToPtr(h);  
  return p->srate == 0 ? 0.0 : (double)p->curFrameCnt / p->srate;
}

double cw::audio_record_play::current_loc_seconds( handle_t h )
{
  audio_record_play_t* p  = _handleToPtr(h);
  if( p->srate == 0 )
    return 0;
  
  return (double)(p->startedFl ? p->curFrameIdx : p->curFrameCnt)/ p->srate;
}

cw::rc_t cw::audio_record_play::exec( handle_t h, const io::msg_t& msg )
{
  rc_t                 rc = kOkRC;
  audio_record_play_t* p  = _handleToPtr(h);
  
  switch( msg.tid )
  {
  case io::kAudioTId:
    if( msg.u.audio != nullptr )
      _audio_callback(p,*msg.u.audio);
    break;

  default:
    rc = kOkRC;
  
  }

  return rc;
}
