#include "tempotracker.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include "midievent.h"
#include "lu_bound.h"

/*
Note: to play a midifile "correctly" till the end: 
   midiplayer_runto(...,[EOT-time]+1,...);

Note on tempotracker/tmap length:
 - no midifile loaded: 0
 - midifile with no events loaded: 1   ["EOT" is at 0]
 otherwise length is >= 2:
 - at the endtime+GRACE of the midifile a "EOT"(-1) tempo-change is added
 - add_to_tmap() will spell out the implicit 120 BPM, if the midifile
   does not have a tempo change at the beginning (abstickpos==0).
*/

// this much extra-time is added for the last note to sound: 
#define GRACE 1000   // msec

struct _ttempo_t {
  int tickpos;
  int timepos; // in milliseconds (should be good enough)
  int usec_per_quarter;  // "EOT"==-1
};

static int tt_bytick_cmp(const void *a,const void *b) // {{{
{
  return ((ttempo_t *)a)->tickpos-((ttempo_t *)b)->tickpos;
}
// }}}

static int tt_bytime_cmp(const void *a,const void *b) // {{{
{
  return ((ttempo_t *)a)->timepos-((ttempo_t *)b)->timepos;
}
// }}}

// TODO? these two can be tweaked for higher precision
static inline int mstimefor(int ticks,int ppq,int usecpq) // {{{
{
  return (ticks*(usecpq/1000)+ppq/2)/ppq;
}
// }}}

static inline int ticksfor(int time,int ppq,int usecpq) // {{{
{
  usecpq/=1000;
  return (time*ppq+usecpq/2)/usecpq;
}
// }}}

static void add_to_tmap(tempotracker_t *tt,int tickpos,int usecpq) // {{{
{
  assert ( (tt)&&(tt->tmap)&&(tt->size>=2) );
  if (tt->len==0) {
    tt->tmap[0].timepos=0;
    if (tickpos>0) { // honour implicit 120 bpm at beginning
      tt->tmap[0].tickpos=0;
      tt->tmap[0].usec_per_quarter=500000;  // 120 BPM
      tt->len++;
    }
  }
  if (tt->len>=tt->size) {
    tt->size*=2;
    ttempo_t *tmp=realloc(tt->tmap,sizeof(tt->tmap[0])*tt->size);
    if (!tmp) {
      free(tt->tmap);
      tt->tmap=NULL;
      return;
    }
    tt->tmap=tmp;
  }
  tt->tmap[tt->len].tickpos=tickpos;
  // .timepos will be set later
  tt->tmap[tt->len].usec_per_quarter=usecpq;
  tt->len++;
}
// }}}

static int load_tempotrack(tempotracker_t *tt,MIDITRACK *track) // {{{  returns ticklength
{
  assert( (tt)&&(tt->tmap)&&(tt->size>=2)&&(track) );

  int curticks=0;
  unsigned char status=0x70;
  midi_event_t ev;
  init_midi_event(&ev);

  unsigned char *pos=track->data,*end=track->data+track->len;
  while (pos<end) {
    const int res=midi_event_read(&ev,&status,pos,end);
    if (!res) {
      break;
    }

    assert(ev.time>=0);
    curticks+=ev.time;
    pos+=res;
    if (midi_event_isEOT_fast(&ev)) {
      break;
    } else if ( (ev.type==EV_META)&&(ev.meta.type==META_TEMPO) ) {
      // add to map
      add_to_tmap(tt,curticks,ev.meta.tempo.usec_per_quarter);
    }
  }
  return curticks;
}
// }}}

// used for non-tempo tracks
static int get_tracklength(MIDITRACK *track) // {{{  in ticks
{
  assert(track);

  int curticks=0;
  unsigned char status=0x70;
  midi_event_t ev;
  init_midi_event(&ev);

  unsigned char *pos=track->data,*end=track->data+track->len;
  while (pos<end) {
    // peek at deltatime
    unsigned char *tmp=pos;
    const int delta=midi_read_vl(&tmp,end);
    if (delta<0) {
      break;
    }
    curticks+=delta;

    const int len=midi_event_skip(&status,pos,end);
    if (len<=0) { // EOT or error
      break;
    }
    if ( (tmp[0]==EV_META)&&(tmp[1]==META_TEMPO) ) {
      fprintf(stderr,"Ignored tempo event on track !=0\n");
    }
    pos+=len;
  }
  return curticks;
}
// }}}

// for fast tick<->time conversion we calculate the elapsed time
// at each tempo-change beforehand (currently with millisecond precision)
static void calculate_times(tempotracker_t *tt) // {{{
{
  assert( (tt)&&(tt->tmap)&&(tt->len>=1)&&(tt->ppq>0) );
  assert( (tt->tmap[0].timepos==0)&&(tt->tmap[0].tickpos==0) );

  int iA;
  for (iA=1;iA<tt->len;iA++) {
    // calculate elapsed time at this section's tempo (-> previous event)
    tt->tmap[iA].timepos=tt->tmap[iA-1].timepos+
                         mstimefor((tt->tmap[iA].tickpos-tt->tmap[iA-1].tickpos),tt->ppq,tt->tmap[iA-1].usec_per_quarter);
  }
}
// }}}


// will consider all tracks to get overall length of mf
void tempotracker_load(tempotracker_t *tt,MIDIFILE *mf) // {{{
{
  if ( (!tt)||(!tt->tmap)||(tt->size<2)||
       (!mf)||(mf->ppq<=0)||(mf->no_tracks<1)||(mf->format>1) ) {
    assert(0);
    return;
  }

  // re-init tmap
  tt->len=0;
  tt->ppq=mf->ppq;

  int allticks=load_tempotrack(tt,mf->tracks[0]);
  int iA;
  for (iA=1;iA<mf->no_tracks;iA++) {
    const int res=get_tracklength(mf->tracks[iA]);
    if (res>allticks) {
      allticks=res;
    }
  }

  // final event
  if (allticks>0) {
    int lasttempo=500000;  // 120 BPM
    if (tt->len) {
      lasttempo=tt->tmap[tt->len-1].usec_per_quarter;
    }
    add_to_tmap(tt,allticks+ticksfor(GRACE,tt->ppq,lasttempo),-1);
  } else {
    add_to_tmap(tt,allticks,-1);
  }

  // NOTE: tmap is already sorted!
  // qsort(tt->tmap,tt->len,sizeof(tt->tmap[0]),tt_bytick_cmp); // if load_tempotrack is used more than once

  calculate_times(tt);
}
// }}}

void init_tempotracker(tempotracker_t *tt) // {{{
{
  if (!tt) {
    return;
  }
  tt->len=0;
  tt->size=10;
  tt->ppq=0;
  tt->tmap=malloc(sizeof(tt->tmap[0])*tt->size);
  // tt->tmap==NULL is error state
}
// }}}

void free_tempotracker_data(tempotracker_t *tt) // {{{
{
  if (!tt) {
    return;
  }
  free(tt->tmap);
  tt->tmap=NULL;
}
// }}}

enum { BY_TICK=true, BY_TIME=false };

static ttempo_t *getforpos(tempotracker_t *tt,int value,bool bytick) // {{{
{
  if ( (!tt)||(!tt->tmap)||(tt->len<1)||(value<0) ) {
    assert(0);
    return NULL;
  }
  // find value in tmap
  ttempo_t key={ .tickpos = value, .timepos = value };
  ttempo_t *ret=ubound(&key,tt->tmap,tt->len,sizeof(tt->tmap[0]),
                       ((bytick)?tt_bytick_cmp:tt_bytime_cmp));
  assert(ret>tt->tmap); // before start?! (either value<0 or tt->tmap[0].?pos>0)
  ret--;
  if (ret->usec_per_quarter==-1) { // at EOT
    return NULL;
  }
  return ret;
}
// }}}

int tempotracker_get_tempo(tempotracker_t *tt,int abstickpos) // {{{
{
  ttempo_t *res=getforpos(tt,abstickpos,BY_TICK);
  if (!res) {
    return -1;
  }
  return res->usec_per_quarter;
}
// }}}

float tempotracker_get_time(tempotracker_t *tt,int abstickpos) // {{{
{
  ttempo_t *res=getforpos(tt,abstickpos,BY_TICK);
  if (!res) {
    return -1;
  }
  const int ret=res->timepos+mstimefor(abstickpos-res->tickpos,tt->ppq,res->usec_per_quarter);
  return ret/1000.; // msec -> sec
}
// }}}

int tempotracker_get_tick(tempotracker_t *tt,float abstime) // {{{
{
  const int mstime=abstime*1000; // sec -> msec
  ttempo_t *res=getforpos(tt,mstime,BY_TIME);
  if (!res) {
    return -1;
  }
  return res->tickpos+ticksfor(mstime-res->timepos,tt->ppq,res->usec_per_quarter);
}
// }}}

int tempotracker_next_tempochange(tempotracker_t *tt,int abstickpos) // {{{
{
  ttempo_t *res=getforpos(tt,abstickpos,BY_TICK);
  if (!res) {
    return tempotracker_get_ticklength(tt);
  }
  res++;
  assert(res<tt->tmap+tt->len); // worst case should be EOT
  return res->tickpos;
}
// }}}

int tempotracker_get_ticklength(tempotracker_t *tt) // {{{
{
  if ( (!tt)||(!tt->tmap)||(tt->len<1) ) {
    assert(0);
    return -1;
  }
  ttempo_t *last=tt->tmap+tt->len-1;
  assert(last->usec_per_quarter==-1);
  return last->tickpos;
}
// }}}

float tempotracker_get_timelength(tempotracker_t *tt) // {{{
{
  if ( (!tt)||(!tt->tmap)||(tt->len<1) ) {
    assert(0);
    return -1;
  }
  ttempo_t *last=tt->tmap+tt->len-1;
  assert(last->usec_per_quarter==-1);
  return last->timepos/1000.; // msec -> sec
}
// }}}

