// rdrenderer.cpp
//
// Render a Rivendell log to a single audio object.
//
//   (C) Copyright 2017 Fred Gleason <fredg@paravelsystems.com>
//
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License version 2 as
//   published by the Free Software Foundation.
//
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public
//   License along with this program; if not, write to the Free Software
//   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//

#include <errno.h>
#include <math.h>

#include "rdaudioconvert.h"
#include "rdaudioexport.h"
#include "rdaudioimport.h"
#include "rdcart.h"
#include "rdconf.h"
#include "rdcut.h"
#include "rdtempdirectory.h"

#include "rdrenderer.h"

__RDRenderLogLine::__RDRenderLogLine(RDLogLine *ll,RDUser *user,
				     RDStation *station,RDSystem *sys,
				     RDConfig *config,unsigned chans)
  : RDLogLine(*ll)
{
  ll_cart=NULL;
  ll_cut=NULL;
  ll_handle=NULL;
  ll_user=user;
  ll_station=station;
  ll_system=sys;
  ll_config=config;
  ll_channels=chans;
  ll_ramp_level=0.0;
  ll_ramp_rate=0.0;
}


RDCart *__RDRenderLogLine::cart() const
{
  return ll_cart;
}


RDCut *__RDRenderLogLine::cut() const
{
  return ll_cut;
}


SNDFILE *__RDRenderLogLine::handle() const
{
  return ll_handle;
}


double __RDRenderLogLine::rampLevel() const
{
  return ll_ramp_level;
}


void __RDRenderLogLine::setRampLevel(double lvl)
{
  ll_ramp_level=lvl;
}


double __RDRenderLogLine::rampRate() const
{
  return ll_ramp_rate;
}


void __RDRenderLogLine::setRampRate(double lvl)
{
  ll_ramp_rate=lvl;
}


void __RDRenderLogLine::setRamp(RDLogLine::TransType next_trans)
{
  if((next_trans==RDLogLine::Segue)&&
     (ll_cut->segueStartPoint()>=0)&&(ll_cut->segueEndPoint()>=0)) {
    ll_ramp_rate=((double)RD_FADE_DEPTH)/
      ((double)FramesFromMsec(ll_cut->segueEndPoint()-ll_cut->segueStartPoint()));
  }
}


bool __RDRenderLogLine::open(const QTime &time)
{
  QString cutname;
  SF_INFO sf_info;

  if(type()==RDLogLine::Cart) {
    ll_cart=new RDCart(cartNumber());
    if(ll_cart->exists()&&(ll_cart->type()==RDCart::Audio)) {
      if(ll_cart->selectCut(&cutname,time)) {
	ll_cut=new RDCut(cutname);
	QString filename;
	if(GetCutFile(cutname,ll_cut->startPoint(),ll_cut->endPoint(),
		      &filename)) {
	  ll_handle=sf_open(filename,SFM_READ,&sf_info);
	  if(ll_handle!=NULL) {
 	    DeleteCutFile(filename);
	    return true;
	  }
	}
      }
    }
  }
  return false;
}


void __RDRenderLogLine::close()
{
  sf_close(ll_handle);
  ll_handle=NULL;
}


QString __RDRenderLogLine::summary() const
{
  QString ret=QString().sprintf("unknown event [type: %d]",type());
  switch(type()) {
  case RDLogLine::Cart:
    ret=QString().sprintf("cart %06u [",cartNumber())+title()+"]";
    break;
	  
  case RDLogLine::Marker:
    ret="marker ["+markerComment()+"]";
    break;

  case RDLogLine::Macro:
    ret="macro cart ["+title()+"]";
    break;

  case RDLogLine::Chain:
    ret="chain-to ["+markerLabel()+"]";
    break;

  case RDLogLine::Track:
    ret="track marker ["+markerComment()+"]";
    break;

  case RDLogLine::MusicLink:
    ret="music link";
    break;

  case RDLogLine::TrafficLink:
    ret="traffic link";
    break;

  case RDLogLine::OpenBracket:
  case RDLogLine::CloseBracket:
  case RDLogLine::UnknownType:
    break;
  }
  return ret;
}


bool __RDRenderLogLine::GetCutFile(const QString &cutname,int start_pt,
				   int end_pt,QString *dest_filename) const
{
  bool ret=false;
  RDAudioConvert::ErrorCode conv_err;
  RDAudioExport::ErrorCode export_err;
  char tempdir[PATH_MAX];
  
  strncpy(tempdir,RDTempDirectory::basePath()+"/rdrenderXXXXXX",PATH_MAX);
  *dest_filename=QString(mkdtemp(tempdir))+"/"+cutname+".wav";
  RDAudioExport *conv=new RDAudioExport(ll_station,ll_config);
  conv->setDestinationFile(*dest_filename);
  conv->setCartNumber(RDCut::cartNumber(cutname));
  conv->setCutNumber(RDCut::cutNumber(cutname));
  RDSettings s;
  s.setFormat(RDSettings::Pcm16);
  s.setSampleRate(ll_system->sampleRate());
  s.setChannels(ll_channels);
  s.setNormalizationLevel(0);
  conv->setDestinationSettings(&s);
  conv->setRange(start_pt,end_pt);
  conv->setEnableMetadata(false);
  switch(export_err=conv->runExport(ll_user->name(),
				    ll_user->password(),&conv_err)) {
  case RDAudioExport::ErrorOk:
    ret=true;
    break;

  default:
    ret=false;
    printf("export err %d [%s]\n",export_err,
	   (const char *)RDAudioExport::errorText(export_err,conv_err));
    break;
  }

  delete conv;
  return ret;
}


void __RDRenderLogLine::DeleteCutFile(const QString &dest_filename) const
{
  unlink(dest_filename);
  QStringList f0=f0.split("/",dest_filename);
  f0.erase(f0.fromLast());
  rmdir("/"+f0.join("/"));
}


uint64_t __RDRenderLogLine::FramesFromMsec(uint64_t msec)
{
  return msec*ll_system->sampleRate()/1000;
}




RDRenderer::RDRenderer(RDUser *user,RDStation *station,RDSystem *system,
		       RDConfig *config,QObject *parent)
  : QObject(parent)
{
  render_user=user;
  render_station=station;
  render_system=system;
  render_config=config;
  render_total_passes=0;
}


RDRenderer::~RDRenderer()
{
}


bool RDRenderer::renderToFile(const QString &outfile,RDLogEvent *log,
			      RDSettings *s,const QTime &start_time,
			      bool ignore_stops,QString *err_msg,
			      int first_line,int last_line,
			      const QTime &first_time,const QTime &last_time)
{
  QString temp_output_filename;
  char tempdir[PATH_MAX];
  bool ok=false;
  FILE *f=NULL;
  bool ret;

  //
  // Verify Destination
  //
  if((f=fopen(outfile,"w"))==NULL) {
    *err_msg=tr("unable to open output file")+" ["+strerror(errno)+"]";
    return false;
  }
  fclose(f);

  if(((s->format()!=RDSettings::Pcm16)&&(s->format()!=RDSettings::Pcm24))||
     (s->normalizationLevel()!=0)) {
    ProgressMessage("Pass 1 of 2");
    render_total_passes=2;

    //
    // Get Temporary File
    //
    strncpy(tempdir,RDTempDirectory::basePath()+"/rdrenderXXXXXX",PATH_MAX);
    temp_output_filename=QString(mkdtemp(tempdir))+"/log.wav";
    ProgressMessage(tr("Using temporary file")+" \""+temp_output_filename+"\".");

    //
    // Render It
    //
    if(!Render(temp_output_filename,log,s,start_time,ignore_stops,err_msg,
	       first_line,last_line,first_time,last_time)) {
      return false;
    }

    //
    // Convert It
    //
    ProgressMessage(tr("Pass 2 of 2"));
    ProgressMessage(tr("Writing output file"));
    ok=ConvertAudio(temp_output_filename,outfile,s,err_msg);
    DeleteTempFile(temp_output_filename);
    emit lineStarted(log->size()+1,log->size()+1);
    if(!ok) {
      return false;
    }
  }
  else {
    ProgressMessage(tr("Pass 1 of 1"));
    render_total_passes=1;

    ret=Render(outfile,log,s,start_time,ignore_stops,err_msg,
	       first_line,last_line,first_time,last_time);
    emit lineStarted(log->size(),log->size());
    return ret;
  }
  return true;
}


bool RDRenderer::renderToCart(unsigned cartnum,int cutnum,RDLogEvent *log,
			      RDSettings *s,const QTime &start_time,
			      bool ignore_stops,QString *err_msg,
			      int first_line,int last_line,
			      const QTime &first_time,const QTime &last_time)
{
  QString temp_output_filename;
  char tempdir[PATH_MAX];
  bool ok=false;

  if(first_line<0) {
    first_line=0;
  }
  if(last_line<0) {
    last_line=log->size();
  }

  //
  // Check that we won't overflow the 32 bit BWF structures
  // when we go to import the rendered log back into the audio store
  //
  if((double)log->length(first_line,last_line-1)/1000.0>=
     (1073741824.0/((double)s->channels()*(double)s->sampleRate()))) {
    *err_msg=tr("Rendered log is too long!");
    return false;
  }

  ProgressMessage(tr("Pass 1 of 2"));
  render_total_passes=2;

  //
  // Verify Destination
  //
  if(!RDCart::exists(cartnum)) {
    *err_msg=tr("no such cart");
    return false;
  }
  if(!RDCut::exists(cartnum,cutnum)) {
    *err_msg=tr("no such cut");
    return false;
  }

  //
  // Get Temporary File
  //
  strncpy(tempdir,RDTempDirectory::basePath()+"/rdrenderXXXXXX",PATH_MAX);
  temp_output_filename=QString(mkdtemp(tempdir))+"/log.wav";
  ProgressMessage(tr("Using temporary file")+" \""+temp_output_filename+"\".");

  //
  // Render It
  //
  if(!Render(temp_output_filename,log,s,start_time,ignore_stops,err_msg,
	     first_line,last_line,first_time,last_time)) {
    return false;
  }

  //
  // Convert It
  //
  ProgressMessage(tr("Pass 2 of 2"));
  ProgressMessage(tr("Importing cart"));
  ok=ImportCart(temp_output_filename,cartnum,cutnum,s->channels(),err_msg);
  DeleteTempFile(temp_output_filename);
  emit lineStarted(log->size()+1,log->size()+1);
  if(!ok) {
    return false;
  }

  return true;
}


QStringList RDRenderer::warnings() const
{
  return render_warnings;
}


void RDRenderer::abort()
{
  render_abort=true;
}


bool RDRenderer::Render(const QString &outfile,RDLogEvent *log,RDSettings *s,
			const QTime &start_time,bool ignore_stops,
			QString *err_msg,int first_line,int last_line,
			const QTime &first_time,const QTime &last_time)
{
  float *pcm=NULL;
  QTime current_time=start_time;
  QString temp_output_filename;

  render_warnings.clear();
  render_abort=false;

  //
  // Open Output File
  //
  SF_INFO sf_info;
  SNDFILE *sf_out;

  memset(&sf_info,0,sizeof(sf_info));
  sf_info.samplerate=render_system->sampleRate();
  sf_info.channels=s->channels();
  if(s->format()==RDSettings::Pcm16) {
    sf_info.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
  }
  else {
    sf_info.format=SF_FORMAT_WAV|SF_FORMAT_PCM_24;
  }
  sf_out=sf_open(outfile,SFM_WRITE,&sf_info);
  if(sf_out==NULL) {
    fprintf(stderr,"rdrender: unable to open output file [%s]\n",
	    sf_strerror(sf_out));
    return 1;
  }

  //
  // Initialize the log
  //
  std::vector<__RDRenderLogLine *> lls;
  for(int i=0;i<log->size();i++) {
    lls.push_back(new __RDRenderLogLine(log->logLine(i),render_user,
					render_station,render_system,
					render_config,s->channels()));
    if(ignore_stops&&(lls.back()->transType()==RDLogLine::Stop)) {
      lls.back()->setTransType(RDLogLine::Play);
    }
    if((!first_time.isNull())&&
       (lls.back()->timeType()==RDLogLine::Hard)&&
       (first_line==-1)&&
       (lls.back()->startTime(RDLogLine::Imported)==first_time)) {
      first_line=i;
    }
    if((!last_time.isNull())&&
       (lls.back()->timeType()==RDLogLine::Hard)&&
       (last_line==-1)&&
       (lls.back()->startTime(RDLogLine::Imported)==last_time)) {
      last_line=i;
    }
  }
  if((!first_time.isNull())&&(first_line==-1)) {
    *err_msg+=tr("first-time event not found");
  }
  if((!last_time.isNull())&&(last_line==-1)) {
    if(!err_msg->isEmpty()) {
      *err_msg+=", ";
    }
    *err_msg+=tr("last-time event not found");
  }
  if(!err_msg->isEmpty()) {
    return false;
  }
  lls.push_back(new __RDRenderLogLine(new RDLogLine(),render_user,
				      render_station,render_system,
				      render_config,s->channels()));
  lls.back()->setTransType(RDLogLine::Play);
  if((!first_time.isNull())&&(first_line==-1)) {
    first_line=log->size();
  }

  //
  // Iterate through it
  //
  for(unsigned i=0;i<lls.size();i++) {
    if(render_abort) {
      emit lineStarted(log->size()+render_total_passes-1,
		       log->size()+render_total_passes-1);
      *err_msg+="Render aborted.\n";
      sf_close(sf_out);
      return false;
    }
    emit lineStarted(i,log->size()+render_total_passes-1);
    if(((first_line==-1)||(first_line<=(int)i))&&
       ((last_line==-1)||(last_line>(int)i))) {
      if(lls.at(i)->transType()==RDLogLine::Stop) {
	ProgressMessage(current_time,i,tr("STOP")+" ",lls.at(i)->summary());
	render_warnings.
	  push_back(tr("log render halted at line")+QString().sprintf(" %d ",i)+
		    tr("due to STOP"));
	break;
      }
      if(lls.at(i)->open(current_time)) {
	ProgressMessage(current_time,i,
			RDLogLine::transText(lls.at(i)->transType()),
		      QString().sprintf(" cart %06u [",lls.at(i)->cartNumber())+
			lls.at(i)->title()+"]");
	sf_count_t frames=0;
	if((lls.at(i+1)->transType()==RDLogLine::Segue)&&
	   (lls.at(i)->cut()->segueStartPoint()>=0)) {
	  frames=FramesFromMsec(lls.at(i)->cut()->segueStartPoint()-
				lls.at(i)->cut()->startPoint());
	  current_time=
	    current_time.addMSecs(lls.at(i)->cut()->segueStartPoint()-
				  lls.at(i)->cut()->startPoint());
	}
	else {
	  frames=FramesFromMsec(lls.at(i)->cut()->endPoint()-
				lls.at(i)->cut()->startPoint());
	  current_time=current_time.addMSecs(lls.at(i)->cut()->endPoint()-
					     lls.at(i)->cut()->startPoint());
	}
	pcm=new float[frames*s->channels()];
	memset(pcm,0,frames*s->channels()*sizeof(float));

	for(unsigned j=0;j<i;j++) {
	  Sum(pcm,lls.at(j),frames,s->channels());
	}
	Sum(pcm,lls.at(i),frames,s->channels());
	sf_writef_float(sf_out,pcm,frames);
	delete pcm;
	pcm=NULL;
	lls.at(i)->setRamp(lls.at(i+1)->transType());
      }
      else {
	if(i<(lls.size()-1)) {
	  if(lls.at(i)->type()==RDLogLine::Cart) {
	    ProgressMessage(current_time,i,tr("FAIL"),lls.at(i)->summary()+
			    " ("+tr("NO AUDIO AVAILABLE")+")");
	    render_warnings.
	      push_back(lls.at(i)->summary()+tr("at line")+
			QString().sprintf(" %d ",i)+
			tr("failed to play (NO AUDIO AVAILABLE)"));
	  }
	  else {
	    ProgressMessage(current_time,i,tr("SKIP"),lls.at(i)->summary());
	  }
	}
	else {
	  ProgressMessage(current_time,lls.size()-1,
			  tr("STOP"),tr("--- end of log ---"));
	}
      }
    }
  }
  sf_close(sf_out);

  return true;
}


void RDRenderer::Sum(float *pcm_out,__RDRenderLogLine *ll,sf_count_t frames,
		     unsigned chans)
{
  if(ll->handle()!=NULL) {
    float *pcm=new float[frames*chans];

    memset(pcm,0,frames*chans);
    sf_count_t n=sf_readf_float(ll->handle(),pcm,frames);
    for(sf_count_t i=0;i<n;i+=chans) {
      double ratio=exp10(((double)i*ll->rampRate()+ll->rampLevel())/2000.0);
      for(sf_count_t j=0;j<chans;j++) {
	pcm_out[i*chans+j]+=ratio*pcm[i*chans+j];
      }
    }
    ll->setRampLevel((double)n*ll->rampRate()+ll->rampLevel());
    if(n<frames) {
      ll->close();
    }
    delete pcm;
  }
}


bool RDRenderer::ConvertAudio(const QString &srcfile,const QString &dstfile,
			      RDSettings *s,QString *err_msg)
{
  RDAudioConvert::ErrorCode err_code;

  RDAudioConvert *conv=new RDAudioConvert(render_station->name(),this);
  conv->setSourceFile(srcfile);
  conv->setDestinationFile(dstfile);
  conv->setDestinationSettings(s);
  err_code=conv->convert();
  *err_msg=RDAudioConvert::errorText(err_code);
  delete conv;

  return err_code==RDAudioConvert::ErrorOk;
}


bool RDRenderer::ImportCart(const QString &srcfile,unsigned cartnum,int cutnum,
			    unsigned chans,QString *err_msg)
{
  RDAudioImport::ErrorCode err_import_code;
  RDAudioConvert::ErrorCode err_conv_code;
  RDSettings settings;
  
  settings.setChannels(chans);
  settings.setNormalizationLevel(0);

  RDAudioImport *conv=new RDAudioImport(render_station,render_config,this);
  conv->setCartNumber(cartnum);
  conv->setCutNumber(cutnum);
  conv->setSourceFile(srcfile);
  conv->setUseMetadata(false);
  conv->setDestinationSettings(&settings);
  err_import_code=
    conv->runImport(render_user->name(),render_user->password(),&err_conv_code);
  *err_msg=RDAudioImport::errorText(err_import_code,err_conv_code);
  delete conv;
  return err_import_code==RDAudioImport::ErrorOk;
}


void RDRenderer::DeleteTempFile(const QString &filename) const
{
  unlink(filename);
  QStringList f0=f0.split("/",filename);
  f0.erase(f0.fromLast());
  rmdir("/"+f0.join("/"));
}


uint64_t RDRenderer::FramesFromMsec(uint64_t msec) const
{
  return msec*render_system->sampleRate()/1000;
}


void RDRenderer::ProgressMessage(const QString &msg)
{
  emit progressMessageSent(msg);
}


void RDRenderer::ProgressMessage(const QTime &time,int line,
				 const QString &trans,const QString &msg)
{
  QString str=QString().sprintf("%04d : ",line)+
    time.toString("hh:mm:ss")+" : "+
    QString().sprintf("%-5s",(const char *)trans)+msg;
  emit progressMessageSent(str);
}
