#include "simplemidi.h"
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include "midifile.h"
#include "midievent.h"
#include "pqueue.h"
#include <stdint.h>
#include <stdio.h>

static inline int parse_hexnum(const char **buf) // {{{
{
  assert( (buf)&&(*buf) );
  const char *b=*buf;

  int ret=0;
  for (;*b;b++) {
    ret<<=4;
    if ( (*b>='0')&&(*b<='9') ) {
      ret|=*b-'0';
    } else if ( (*b>='a')&&(*b<='f') ) {
      ret|=*b-'a'+10;
    } else if ( (*b>='A')&&(*b<='F') ) {
      ret|=*b-'A'+10;
    } else {
      ret>>=4;
      break;
    }
  }
 
// TODO?
//  if (b==*buf) { // nothing read
//  }
  *buf=b;
  return ret;
}
// }}}

typedef struct {
  int abstime,note,duration;  // note==-1 is EOF
} simpletoken_t;

// returns length (or -1), ret may be NULL
int parse_simplemidi(const char *buf,simpletoken_t *ret) // {{{
{
  int len=0;
  int curtime=0;

  buf+=strspn(buf," ");
  while (*buf) {
    const int reltime=parse_hexnum(&buf);
    if (*buf!=':') {
      return -1;
    }
    buf++;
    const int note=parse_hexnum(&buf);
    if (*buf!=':') {
      return -1;
    }
    buf++;
    const int duration=parse_hexnum(&buf);
    if (ret) {
      ret[len]=(simpletoken_t){ 
        .abstime = curtime+reltime,
        .note = note,
        .duration = duration
      };
      curtime+=reltime;
    }
    len++;
    buf+=strspn(buf," ");
  }
  return len;
}
// }}}

// retlen can be NULL,  on error: will be set to
// -2: alloc error, -3: parse error
simpletoken_t *simplemidi(const char *buf,int *retlen) // {{{  or NULL
{
  assert(buf);

  simpletoken_t *tokens=NULL;
  const int len=parse_simplemidi(buf,NULL);
  if (len==-1) {
    if (retlen) {
      *retlen=-3;
    }
    return NULL;
  }

  tokens=malloc(sizeof(*tokens)*(len+1));
  if (!tokens) {
    if (retlen) {
      *retlen=-2;
    }
    return NULL;
  }
  const int len2=parse_simplemidi(buf,tokens);
  assert(len2==len);

  tokens[len]=(simpletoken_t){ .note = -1 };
  if (retlen) {
    *retlen=len;
  }

  return tokens;
}
// }}}

// requires label "error:"
#define ADD_TO_TRACK(track,ev,status) \
  { int res; (track)->len+=res=midi_event_write((ev),(status),(track)->data+(track)->len,(track)->data+(track)->len+7); \
    if (!res) goto error; }

// ticktime==-1: everything remaining
static int note_offs(PQUEUE *noteoffs,MIDITRACK *track,midi_event_t *ev,unsigned char *status,int *curtime,int ticktime) // {{{
{
  ev->note.value=0;
  int next;
  while (  ((next=peek_pq(noteoffs))>=0)&&
           ( (next<=ticktime)||(ticktime==-1) )  ) {
    const intptr_t data=get_pq(noteoffs);
    ev->time=next-*curtime;
    *curtime=next;
    ev->note.tune=data&0xff;
//    ev->channel=data>>8;
    ADD_TO_TRACK(track,ev,status);
  }
  return 0;
error:
  return -1;
}
// }}}

static int sm_processor(simpletoken_t *tokens,unsigned char *status,char channel,MIDITRACK *track) // {{{
{
  assert( (tokens)&&(track) );

  PQUEUE nextnoteoff;
  init_pq(&nextnoteoff,128,PQ_MODE_LOWEST);

  midi_event_t ev;
  init_midi_event(&ev);
  ev.time=0;
  ev.type=EV_NOTE_ON;
  ev.channel=channel;

  int curtime=0;
  simpletoken_t *t=tokens;
  for (;t->note!=-1;t++) {
//printf(" %d %d %d \n",t->abstime,t->note,t->duration);
    if ( (t->note<0)||(t->note>127) ) {
      free_pq_data(&nextnoteoff);
      return -4;
    }
    if (note_offs(&nextnoteoff,track,&ev,status,&curtime,t->abstime)!=0) {
      goto error;
    }
    // new note on
    ev.time=t->abstime-curtime;
    curtime=t->abstime;
    ev.note.tune=t->note;
    ev.note.value=100;
    ADD_TO_TRACK(track,&ev,status);
    // schedule note off
    add_pq(&nextnoteoff,t->abstime+t->duration,t->note);
  }
  // all remaining note_offs
  if (note_offs(&nextnoteoff,track,&ev,status,&curtime,-1)!=0) {
    goto error;
  }

  free_pq_data(&nextnoteoff);
  return 0;
error:
  free_pq_data(&nextnoteoff);
  return -5;
}
// }}}

int simplemidis_to_midi(const char **smidis,int slen,int ppq,int tempo,MIDIFILE *mf) // {{{
{
  if (!mf) {
    assert(0);
    return -1;
  } 
  init_midifile(mf);

  if ( (slen<=0)||(!smidis)||(!*smidis) ) {
    assert(0);
    return -1;
  } 

  // make rudimentary midifile
  mf->format=1;
  mf->no_tracks=slen;
  mf->ppq=ppq;
  mf->tracks=calloc(slen,sizeof(MIDITRACK*));
  if (!mf->tracks) {
    return -2;
  }

  // allocate temporary storage
  simpletoken_t **tokens=calloc(slen,sizeof(simpletoken_t*));
  if (!tokens) {
    return -2;
  }

  int ret=0,iA;
  // parse smidis
  for (iA=0;iA<slen;iA++) {
    tokens[iA]=simplemidi(smidis[iA],&ret);
    if (!tokens[iA]) {
      goto leave; // error code in ret;
    }

    // worst case: sizeof(event)*((off+on)*len + extra events)
//printf("%d\n", (4+3)*(2*len+2) );
    mf->tracks[iA]=calloc((4+3)*(2*ret+2),sizeof(MIDITRACK)); // len in ret, don't leak garbage
    if (!mf->tracks[iA]) {
      ret=-2;
      goto leave;
    }
    // mf->tracks[iA]->len=0;   already done
  }

  // ... we can finally begin ...
  unsigned char status=0x70;
  midi_event_t ev;
  init_midi_event(&ev);
  ev.time=0;
  ev.type=EV_META;

  // write tempo (only first track)
  ev.meta.type=META_TEMPO;
  ev.meta.tempo.usec_per_quarter=60000000/tempo;
  ADD_TO_TRACK(mf->tracks[0],&ev,&status);

  // prepare EOT
  ev.meta.type=META_EOT;

  for (iA=0;iA<slen;iA++) {
    MIDITRACK *track=mf->tracks[iA];

    // write notes
    ret=sm_processor(tokens[iA],&status,iA%16,track);
    if (ret!=0) {
      goto leave;
    }

    // write EOT
    ADD_TO_TRACK(track,&ev,&status);

    status=0x70;
  }

//printf("%d\n",track->len);

leave:
  for (iA=0;iA<slen;iA++) {
    free(tokens[iA]);
  }
  free(tokens);
  return ret;
error:
  ret=-5;
  goto leave;
}
// }}}

int simplemidi_to_midi(const char *smidi,int ppq,int tempo,MIDIFILE *mf) // {{{
{
  return simplemidis_to_midi(&smidi,1,ppq,tempo,mf);
}
// }}}

#undef ADD_TO_TRACK

