#include "AS3.h"
#include "fluidsynth.h"
#include "audioout.h"
#include "midi2fluid.h"
#include "simplemidi.h"
#include "midifile.h"

typedef struct {
  fluid_settings_t *settings;
  fluid_synth_t *synth;

  int upsample;
  float last[2];

  audio_out_t ao;
} mf_t;

//  init(sfFile:String,sfBank:int=0,sfPreset:int=0):void
static AS3_Val mf_init(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  int sfBank=0;
  int sfPreset=0;
  char *sfFile="./example.sf2";
  AS3_ArrayValue(params,"StrType,IntType,IntType",&sfFile,&sfBank,&sfPreset);

  int id=fluid_synth_sfload(mfdata->synth,sfFile,0);
  // Select bank and preset on all channels from the SoundFont
  int iA;
  for (iA=0;iA<16;iA++) {
    fluid_synth_program_select(mfdata->synth,iA,id,sfBank,sfPreset);
  }
  // drums
  fluid_synth_program_select(mfdata->synth,9,id,128,sfPreset);

  return AS3_Null();
}
// }}}

//  load(midiFile:String):Boolean
static AS3_Val mf_load(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if (mfdata->ao.player) {
    free_midiplayer(mfdata->ao.player);
  }

  char *midifile=NULL;
  AS3_ArrayValue(params,"StrType",&midifile);
  mfdata->ao.player=midiplayer_load(midifile);
  if (!mfdata->ao.player) {
    return AS3_False();
  }

  ao_set_tempo_scaling(&mfdata->ao,1.0); // TODO? elsewhere (ao_simple_seek?)
  ao_simple_seek(&mfdata->ao,-1);
  return AS3_True();
}
// }}}

//  loadSimple(simpleMidi:String/Array,num:int,ppq:int,bpm:int):Boolean    num=0:String,>0:Array !
static AS3_Val mf_load_simple(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if (mfdata->ao.player) {
    free_midiplayer(mfdata->ao.player);
    mfdata->ao.player=NULL;
  }

  AS3_Val smval=AS3_Undefined();
  int slen=-1,ppq=-1,bpm=-1;
  AS3_ArrayValue(params,"AS3ValType,IntType,IntType,IntType",&smval,&slen,&ppq,&bpm);
  if ( (!smval)||(slen<0)||(ppq<=0)||(bpm<=0) ) {
    return AS3_False();
  }

  MIDIFILE *mf=malloc(sizeof(MIDIFILE));
  if (!mf) {
    return AS3_False();
  }

  int iA=1;
  char **smidis=NULL;

  if (slen>0) { // we expect an array
    smidis=calloc(slen,sizeof(char*));
    if (!smidis) {
      free_midifile(mf);
      return AS3_False();
    }

    for (iA=0;iA<slen;iA++) {
      AS3_Val tmp=AS3_Int(iA);
      AS3_Val tmp2=AS3_Get(smval,tmp);
      AS3_Release(tmp);
      if (!tmp2) {
        break;
      }
      smidis[iA]=AS3_StringValue(tmp2);
      AS3_Release(tmp2);
    }
  } else { // expect string
    slen=1;
    smidis=malloc(sizeof(char*));
    if (!smidis) {
      free_midifile(mf);
      return AS3_False();
    }
    smidis[0]=AS3_StringValue(smval);
  }

  int res=-1;
  if (iA==slen) { // success, so far
    res=simplemidis_to_midi((const char **)smidis,slen,ppq,bpm,mf);
  }

  // free smidis
  for (iA=0;iA<slen;iA++) {
    free(smidis[iA]);
  }
  free(smidis);

  // finally we can bail out
  if (res!=0) {
    free_midifile(mf);
    return AS3_False();
  }

  mfdata->ao.player=midiplayer_from_midi(mf); // takes mf
  if (!mfdata->ao.player) {
    return AS3_False();
  }

  ao_set_tempo_scaling(&mfdata->ao,1.0); // TODO? elsewhere (ao_simple_seek?)
  ao_simple_seek(&mfdata->ao,-1);
  return AS3_True();
}
// }}}

//  position():Number
static AS3_Val mf_position(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if (!mfdata->ao.player) {
    return AS3_Number(-1);
  }
  return AS3_Number(midiplayer_gettime(mfdata->ao.player));
}
// }}}

//  length():Number
static AS3_Val mf_length(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if (!mfdata->ao.player) {
    return AS3_Number(-1);
  }
  return AS3_Number(midiplayer_getlength(mfdata->ao.player));
}
// }}}

//  isDone():Boolean
static AS3_Val mf_isDone(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if ( (!mfdata->ao.player)||(midiplayer_isdone(mfdata->ao.player)) ) {
    return AS3_True();
  }
  return AS3_False();
}
// }}}

//  seek(time:Number):void
static AS3_Val mf_seek(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  double time=-1;
  AS3_ArrayValue(params,"DoubleType",&time);
  if (time>=0) {
    ao_simple_seek(&mfdata->ao,time);
  }
  return AS3_Null();
}
// }}}

//  getTempoScale():Number
static AS3_Val mf_getTempoScale(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  return AS3_Number(mfdata->ao.temposcale);
}
// }}}

//  setTempoScale(scale:Number):void
static AS3_Val mf_setTempoScale(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  double scale=0;
  AS3_ArrayValue(params,"DoubleType",&scale);
  if ( (scale>0)&&(scale<100) ) {
    ao_set_tempo_scaling(&mfdata->ao,scale);
  }
  return AS3_Null();
}
// }}}

//  tempo():Number
static AS3_Val mf_tempo(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  if (!mfdata->ao.player) {
    return AS3_Number(-1);
  }
  int upq=midiplayer_gettempo(mfdata->ao.player);
  if (upq<0) {
    return AS3_Number(-1);
  }
  return AS3_Number(60000000.0/upq);
}
// }}}

// TODO? return something? e.g. num samples to next event
//   execMidi(numSamples:uint):void
static AS3_Val mf_execMidi(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;

  unsigned int numSamples=64;
  AS3_ArrayValue(params,"IntType",&numSamples);
  ao_execute_midi(&mfdata->ao,numSamples);

  return AS3_Null();
}
// }}}

// big endian floats  for flash 
static inline float swap_alc(float in) // {{{
{
  union
  {
    float f;
    unsigned char b[4];
  } dat1, dat2;

  dat1.f=in;
  dat2.b[0]=dat1.b[3];
  dat2.b[1]=dat1.b[2];
  dat2.b[2]=dat1.b[1];
  dat2.b[3]=dat1.b[0];

  return dat2.f;
}
// }}}

//   getData(outData:ByteArray,numSamples:uint):void   // TODO? FIXME currently only numSamples<=2048 !!
static AS3_Val mf_getData(void *data,AS3_Val params) // {{{
{
  mf_t *mfdata=data;
  if (!mfdata->synth) {
    return AS3_Null();
  }

  unsigned int numSamples=2048;
  AS3_Val outData=AS3_Undefined();
  AS3_ArrayValue(params,"AS3ValType,IntType",&outData,&numSamples);

  numSamples/=mfdata->upsample;
  float out[2*8192];
  fluid_synth_write_float(mfdata->synth,numSamples,out,2*mfdata->upsample-2,2*mfdata->upsample,out,2*mfdata->upsample-1,2*mfdata->upsample);

  int iA,iB;
  const int upsample=mfdata->upsample;
  const float mult=1./upsample;
  int outpos=0;
  for (iA=0;iA<numSamples;iA++,outpos+=2) {
    const float a0=mfdata->last[0],b0=out[outpos+2*upsample-2],d0=(b0-a0)*mult;
    const float a1=mfdata->last[1],b1=out[outpos+2*upsample-1],d1=(b1-a1)*mult;
    for (iB=1;iB<upsample;iB++,outpos+=2) {
      out[outpos]=swap_alc(a0+d0*iB);
      out[outpos+1]=swap_alc(a1+d1*iB);
    }
    out[outpos]=swap_alc(b0);
    out[outpos+1]=swap_alc(b1);
    mfdata->last[0]=b0;
    mfdata->last[1]=b1;
  }
  AS3_ByteArray_writeBytes(outData,out,numSamples*mfdata->upsample*sizeof(float)*2);

fflush(stdout); // HACK to get Alchemy output ANYTHING
        
  AS3_Release(outData);
  return AS3_Null();
}
// }}}

mf_t mfdata; // inside main() does not work

int main()
{
  int samplerate=44100;

  mfdata.upsample=2;
  mfdata.last[0]=mfdata.last[1]=0;

  mfdata.settings=new_fluid_settings();
  fluid_settings_setnum(mfdata.settings,"synth.sample-rate",samplerate/mfdata.upsample);
//  fluid_settings_setint(mfdata.settings,"synth.verbose",1);
  mfdata.synth=new_fluid_synth(mfdata.settings);

  ao_setup(&mfdata.ao,NULL,samplerate,(MIDIPLAYER_OUTFN)midi2fluid_event,mfdata.synth);

  AS3_Val vals[]={AS3_Function(&mfdata,mf_init),
                  AS3_Function(&mfdata,mf_load),
                  AS3_Function(&mfdata,mf_load_simple),

                  AS3_Function(&mfdata,mf_position),
                  AS3_Function(&mfdata,mf_length),
                  AS3_Function(&mfdata,mf_isDone),
                  AS3_Function(&mfdata,mf_seek),
                  AS3_Function(&mfdata,mf_getTempoScale),
                  AS3_Function(&mfdata,mf_setTempoScale),
                  AS3_Function(&mfdata,mf_tempo),

                  AS3_Function(&mfdata,mf_execMidi),
                  AS3_Function(&mfdata,mf_getData)
                  };

  AS3_Val obj=AS3_Object("version:StrType,"
                         "init:AS3ValType,"
                         "load:AS3ValType,"
                         "loadSimple:AS3ValType,"

                         "position:AS3ValType,"
                         "length:AS3ValType,"
                         "isDone:AS3ValType,"
                         "seek:AS3ValType,"
                         "getTempoScale:AS3ValType,"
                         "setTempoScale:AS3ValType,"
                         "tempo:AS3ValType,"

                         "execMidi:AS3ValType,"
                         "getData:AS3ValType",

                         "0.5",vals[0],vals[1],vals[2],
                         vals[3],vals[4],vals[5],vals[6],vals[7],vals[8],vals[9],
                         vals[10],vals[11]);

  int iA;
  for (iA=0;iA<sizeof(vals)/sizeof(vals[0]);iA++) {
    AS3_Release(vals[iA]);
  }
  AS3_LibInit(obj); // does not return!

  free_midiplayer(mfdata.ao.player);
  delete_fluid_synth(mfdata.synth);
  delete_fluid_settings(mfdata.settings);
  return 0;
}
