/*
  Several functions copied from midifile.c, which is
  from project 'abcmidi' by Seymour Shlien and others.
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <SDL/SDL_stdinc.h> // for datatypes
#include "bigband.h"
#include "midi-in.h"
#include "gm.h"  // GM instruments and percussion
#include "str.h"

static FILE *midi_f,
            *gm_map;

const int msgbuf_max=1000;
static int Mf_toberead,
           Mf_bytesread,
           Mf_currtime,
           Msgindex,
           ntrks,
           division=4,  // microseconds per beat
           instr_map[16],
           transp_map[16],
           color_map[128],
           map_format;
static bool or_alert,
            perc_map[128];
static char Msgbuff[msgbuf_max];
static float t_incr=1.;

struct Midi_Note {
  Uint8 lnr,
        occ;  // occupied?
  bool valid; // not out of range?
  int start_time;
} note_arr[128][16];

struct MidiPercNote {
  Uint8 occ;   // occupied?
} perc_note_arr[perc_hi+1];

MidiIn midi_in;

int get_perc_instr(int midi_nr) {
  if (midi_nr<perc_lo || midi_nr>perc_hi) {
    or_alert=true;
    return 5;
  }
  return gm_perc[midi_nr-perc_lo].col;
}

int egetc() {
  int c = getc(midi_f);

  if (c == EOF) { alert("unexpected end-of-file"); return c; }
  Mf_toberead--;
  Mf_bytesread++;
  return c;
}

int readvarinum()
{
  int value;
  int c;

  c = egetc();
  value = c;
  if ( c & 0x80 ) {
    value &= 0x7f;
    do {
      c = egetc();
      value = (value << 7) + (c & 0x7f);
    } while (c & 0x80);
  }
  return value;
}

int to32bit(int c1,int c2,int c3,int c4) {
  int value = 0;

  value = (c1 & 0xff);
  value = (value<<8) + (c2 & 0xff);
  value = (value<<8) + (c3 & 0xff);
  value = (value<<8) + (c4 & 0xff);
  return value;
}

int to16bit(int c1, int c2) {
  return ((c1 & 0xff ) << 8) + (c2 & 0xff);
}

int read32bit() {
  int c1, c2, c3, c4;

  c1 = egetc();
  c2 = egetc();
  c3 = egetc();
  c4 = egetc();
  return to32bit(c1,c2,c3,c4);
}

int read16bit() {
  int c1, c2;
  c1 = egetc();
  c2 = egetc();
  return to16bit(c1,c2);
}

MidiIn::MidiIn() {
}

void midi_note_on(int chan,int time,int instr,int midi_nr) {
  int lnr;
  if (midi_nr<0 || midi_nr>=128)   // can happen after transpose
    return;
  Midi_Note *nb=note_arr[midi_nr]+chan;
  ++nb->occ;
  if (nb->occ==1) {  // else no updating
    nb->start_time=time;
    lnr=midinr_to_lnr(midi_nr);
    if (lnr>=0) {
      nb->valid=true;
      nb->lnr=lnr;
    }
    else {
      nb->valid=false;
      alert("bad note, time=%d, midi-nr=%d, channel=%d",time/subdiv,midi_nr,chan+1);
    }
  }
}

void midi_note_off(int chan,int time,int instr,int midi_nr) {
  Midi_Note *nb=note_arr[midi_nr]+chan;
  if (nb->occ) {
    --nb->occ;
    if (nb->valid) {
      insert_midi_note(nb->start_time,time,nb->lnr,instr); // in bigband.cpp
    }
  }
  else {
    alert("note %d lost, channel %d, time",midi_nr,chan+1,time*4/division);
  }
}

void midi_perc_on(int chan,int time,int midi_nr) { // midi_nr: 35 - 81
  MidiPercNote *nb=perc_note_arr+midi_nr;
  ++nb->occ;
  int instr=get_perc_instr(midi_nr);
  insert_midi_perc(time,instr);
}

void midi_perc_off(int time,int midi_nr) {
  if (midi_nr<perc_lo || midi_nr>perc_hi)
    return;
  MidiPercNote *nb=perc_note_arr+midi_nr;
  if (nb->occ)
    --nb->occ;
  else {
    alert("perc note %d lost, time %d",midi_nr,time*4/division);
  }
}

void midi_noteOn(int chan,int time,int instr,int midi_nr,int transp) {
  if (debug) printf("noteOn: chan=%d time=%d instr=%d nr=%d transp=%d\n",chan,time,instr,midi_nr,transp);
  if (chan==9)  // percussion
    midi_perc_on(chan,time,midi_nr);
  else
    midi_note_on(chan,time,instr,midi_nr+transp);
}

void midi_noteOff(int chan,int time,int instr,int midi_nr,int transp) {
  if (debug) printf("noteOff: chan=%d time=%d instr=%d nr=%d\n",chan,time,instr,midi_nr);
  if (chan==9)  // percussion
    midi_perc_off(time,midi_nr);
  else
    midi_note_off(chan,time,instr,midi_nr+transp);
}

int cur_time() { // rounded to nearest integer
  return lrint(subdiv*Mf_currtime*nupq*t_incr/division);
}

bool MidiIn::chanmessage(int status,int c1,int c2) {
  int chan = status & 0xf;
  status = status & 0xf0;
  switch (status) {
    case 0x80:
    case 0x90: {
        int col,
            transp;
        if (read_mapf) {
          if (chan==9) {
            if (map_format==4 && !perc_map[c1]) {
              perc_map[c1]=true;
              int perc_nr;
              char buf[50];
              Str str(gm_map,buf);
              str.rword("%d",&perc_nr);
              if (str.ch==EOF) { alert("unexpected EOF in .gm-map file"); return false; }
              if (perc_nr!=c1) { alert("mismatch in .gm-map file"); return false; }
              if (str.ch=='"')
                str.rword("\"%[^\"]\"",buf);
              else
                str.rword("%s",buf);
              int c;
              str.rword("%d",&c); // color
              if (c<0 || c>=perc_max) { alert("perc nr %d in .gm-map file (should be 0 - %d)",c,perc_max-1); return false; }
              gm_perc[c1-perc_lo].col=c;
            }
            col=gm_perc[c1-perc_lo].col;
          }
          else
            col=color_map[instr_map[chan]];
          if (col>=0) {  // else no mapping
            transp=transp_map[chan];
            if (status==0x90 && c2>0)
              midi_noteOn(chan,cur_time(),col,c1,transp);
            else
              midi_noteOff(chan,cur_time(),col,c1,transp);
          }
        }
        else {
          if (chan==9) {
            if (!perc_map[c1]) {
              perc_map[c1]=true;
              GM_perc *gmp=gm_perc+c1-perc_lo;
              char name[41]; name[40]=0;
              snprintf(name,40,"\"%s\"",gmp->name);
              fprintf(gm_map,"%-5d %-20s %d\n",gmp->gm_perc_nr,name,gmp->col);
            }
          }
        }
      }
      break;
    case 0xa0:
      if (debug) puts("Mf_pressure");
      break;
    case 0xb0:
      if (debug) printf("Mf_parameter: %d %d %d\n",chan,c1,c2);
      break;
    case 0xe0:
      if (debug) puts("Mf_pitchbend");
      break;
    case 0xc0:
      if (debug) printf("Mf_program: chan=%d c1=%d\n",chan,c1);
      instr_map[chan]=c1;
      if (read_mapf) {
        int chan_nr,
            col_nr=0,
            transp=0;
        char buf[50];
        Str str(gm_map,buf);
        str.rword("%d",&chan_nr);
        if (str.ch==EOF) { alert("unexpected EOF in .gm-map file"); return false; }
        --chan_nr;
        if (str.ch=='"')
          str.rword("\"%[^\"]\"",buf);
        else
          str.rword("%s",buf);
        if (chan!=9) {
          str.rword("%d",&col_nr); // color
        }
        if (chan==9) {  // percussion?
          if (str.ch!='\n') {
            alert("wrong nr items in .gm-map file (expected 2)");
            return false;
          }
        }
        else {
          if (str.ch!='\n') {
            str.rword("%d",&transp);
            if (transp>5 || transp<-5) {
              alert("transpose %d octaves?",transp);
              transp=0;
            }
          }
          if (str.ch!='\n') {
            alert("wrong nr items in .gm-map file (expected 3 or 4)");
            return false;
          }
        }
        if (chan!=chan_nr) {
          alert("channel = %d (expected %d)",chan_nr+1,chan+1);
          return false;
        }
        if (chan!=9) {
          if (col_nr>=inst_max) {
            alert("instr nr %d in .gm-map file (should be 0 - %d)",col_nr,inst_max-1);
            col_nr=-1;
          }
          color_map[c1]= col_nr<0 ? -1 : col_nr;  // -1 -> no mapping
        }
        transp_map[chan]=transp*12;
      }
      else {
        if (chan==9) {
          fprintf(gm_map,"10 PERCUSSION\n");
        }
        else {
          char name[41]; name[40]=0;
          snprintf(name,40,"\"%s\"",gm_instr[c1].name);
          fprintf(gm_map,"%-3d %-20s 0\n",chan+1,name);
        }
      }
      break;
    case 0xd0:
      if (debug) puts("Mf_chanpressure");
      break;
    default:
      printf("chanmessage: %x\n",status);
  }
  return true;
}

void msgadd(int c) {
  if (Msgindex >= msgbuf_max-1) {
    alert("msgadd: buf overflow");
  }
  else {
    Msgbuff[Msgindex++] = c;
    Msgbuff[Msgindex] = 0;
  }
}

void metaevent(int type) {
  int leng = Msgindex;
  char *m = Msgbuff;

  switch  ( type ) {
    case 0x00:
      if (debug) printf("Mf_seqnum: %d\n",to16bit(m[0],m[1]));
      break;
    case 0x01:  /* Text event */
    case 0x02:  /* Copyright notice */
    case 0x03:  /* Sequence/Track name */
    case 0x04:  /* Instrument name */
    case 0x05:  /* Lyric */
    case 0x06:  /* Marker */
    case 0x07:  /* Cue point */
    case 0x08:
    case 0x09:
    case 0x0a:
    case 0x0b:
    case 0x0c:
    case 0x0d:
    case 0x0e:
    case 0x0f:
      /* These are all text events */
      if (debug) printf("Mf_text: %d %d '%s'\n",type,leng,m);
      break;
    case 0x21:
      if (debug) printf("Mf_port: %d %d\n",m[0],m[1]);
      break;
    case 0x2f:  /* End of Track */
      if (debug) puts("Mf_eot");
      break;
    case 0x51:  /* Set tempo */
      if (debug) printf("Mf_tempo: %d %d %d\n",m[0],m[1],m[2]);
      break;
    case 0x54:
      if (debug) printf("Mf_smpte: %d %d %d %d %d\n",m[0],m[1],m[2],m[3],m[4]);
      break;
    case 0x58:
      switch (m[1]) {
        case 1: nupq=meter/2; break;
        case 2: nupq=meter/4; break;
        case 3: nupq=meter/8; break;
        default: alert("m[1]=%d",m[1]); break;
      }
      if (debug) {
        printf("Mf_timesig: %d %d %d %d ",m[0],m[1],m[2],m[3]);
        printf("nupq=%d\n",nupq);
      }
      break;
    case 0x59:
      if (debug) printf("Mf_keysig: %d %d\n",m[0],m[1]);
      break;
    case 0x7f:
      if (debug) printf("Mf_seqspecific %d '%s'\n",leng,m);
      break;
    default:
      if (debug) printf("Mf_metamisc: 0x%x %d '%s'\n",type,leng,m);
  }
}

void sysex()
{
  if (debug) printf("Mf_sysex: %d %s\n",Msgindex,Msgbuff);
}

bool MidiIn::readtrack() {    /* read a track chunk */
  /* This array is indexed by the high half of a status byte.  It's */
  /* value is either the number of bytes needed (1 or 2) for a channel */
  /* message, or 0 (meaning it's not  a channel message). */
  static int chantype[] = {
    0, 0, 0, 0, 0, 0, 0, 0,    /* 0x00 through 0x70 */
    2, 2, 2, 2, 1, 1, 2, 0    /* 0x80 through 0xf0 */
  };
  char buf[10];
  int lookfor,
      c, c1, type,
      status = 0,    /* status value (e.g. 0x90==note-on) */
      needed,
      varinum;
  bool sysexcontinue = false,  /* 1 if last message was an unfinished sysex */
       running = false;        /* 1 when running status used */

  if (1!=fread(buf,4,1,midi_f)) return false;
  if (strncmp(buf,"MTrk",4)) {
    alert("track start 'MTrk' not found");
    return false;
  }

  Mf_toberead = read32bit();
  Mf_currtime = 0;
  Mf_bytesread =0;

  if (debug) printf("Mf_trackstart, %d bytes\n",Mf_toberead);

  while ( Mf_toberead > 0 ) {

    Mf_currtime += readvarinum();  /* delta time */

    c = egetc();

    if ( sysexcontinue && c != 0xf7 ) {
      alert("didn't find expected continuation of a sysex");
      return false;
    }
    if ( (c & 0x80) == 0 ) {   /* running status? */
      if ( status == 0 ) {
        alert("unexpected running status");
        return false;
      }
      running = true;
    }
    else {
      status = c;
      running = false;
    }

    needed = chantype[ (status>>4) & 0xf ];

    if ( needed ) {    /* ie. is it a channel message? */
      if ( running )
        c1 = c;
      else
        c1 = egetc();
      if (!chanmessage( status, c1, (needed>1) ? egetc() : 0 )) {
        return false;
      }
      continue;
    }

    switch ( c ) {
      case 0xff:      /* meta event */
        type = egetc();
        if (debug) printf("Meta event, type=0x%x\n",type);
        varinum = readvarinum();
        lookfor = Mf_toberead - varinum;
        Msgindex = 0;

        while ( Mf_toberead > lookfor )
          msgadd(egetc());

        metaevent(type);
        break;

      case 0xf0:    /* start of system exclusive */
        if (debug) printf("Start sysex\n");
        varinum = readvarinum();
        lookfor = Mf_toberead - varinum;
        Msgindex = 0;
        msgadd(0xf0);
  
        while ( Mf_toberead > lookfor )
          msgadd(c=egetc());
  
        if ( c==0xf7 )
          sysex();
        else
          sysexcontinue = true;  /* merge into next msg */
        break;
  
      case 0xf7:  /* sysex continuation or arbitrary stuff */
        if (debug) printf("Sysex continuation\n");

        varinum = readvarinum();
        lookfor = Mf_toberead - varinum;
  
        if ( ! sysexcontinue )
          Msgindex = 0;
  
        while ( Mf_toberead > lookfor )
          msgadd(c=egetc());
  
        if (!sysexcontinue) {
          if (debug) printf("Mf_arbitrary: %d %s\n",Msgindex,Msgbuff);
        }
        else if ( c == 0xf7 ) {
          sysex();
          sysexcontinue = 0;
        }
        break;
      default:
        alert("track: unexpected byte %x",c);
        if (debug) printf("UNEXPECTED BYTE %x\n",c);
        break;
    }
  }
  if (debug) puts("end of track");
  return true;
}

bool MidiIn::read_mf(const char* midi_fn,const char* i_map_fn) {
  static char buf[10];
  int i,i2,
      format=0,
      track;
  key_nr=0;
  or_alert=false;
  gm_map=0;
  meter=16; // meter and nupq may be modified by "tinc" in .gm-map file, and by midi meta event 0x58
  nupq=4;
  if (read_mapf) {
    gm_map=fopen(i_map_fn,"r");
    if (!gm_map) {
      alert("%s not opened",i_map_fn);
      return false;
    }
    char sbuf[50];
    Str str(gm_map,sbuf);
    do {
      str.rword("%[^=\n]=",sbuf);
      if (str=="format") {
        int n;
        str.rword("%d",&n);
        if (n!=3 && n!=4) { alert("%s: expected format = 3 or 4",i_map_fn); return false; }
        map_format=n;
      }
      else if (str=="tinc") {
        str.rword("%f",&t_incr);
      }
      else {
        alert("unknown parameter '%s' in %s",sbuf,i_map_fn);
        return false;
      }
    } while (str.ch!='\n' && str.ch!=str.cmt_ch);
  }
  if ((midi_f=fopen(midi_fn,"r"))==0) {
    alert("midi file '%s' not found",midi_fn);
    return false;
  }
  if (!read_mapf) {
    gm_map=fopen(i_map_fn,"w");
    if (!gm_map) {
      alert("%s not opened",i_map_fn);
      return false;
    }
    fputs("format=4\n\n",gm_map);
    fputs("# channel nr | track name | mapped instr | (transpose)\n",gm_map);
    fputs("# midi perc nr | perc name | mapped perc instr\n\n",gm_map);
  }
  for (i=0;i<16;++i) {
    instr_map[i]=0;
    transp_map[i]=0;
  }
  for (i=0;i<128;++i) {
    color_map[i]=gm_instr[i].col;
    perc_map[i]=false;
    for (i2=0;i2<16;++i2) note_arr[i][i2].occ=0;
  }
  for (i=0;i<=perc_hi;++i)
    perc_note_arr[i].occ=0;
  if (1!=fread(buf,4,1,midi_f)) return false;
  if (strncmp(buf,"MThd",4)) {
    alert("unexpected start '%s' in %s (should be 'MThd')",buf,midi_fn);
    fclose(midi_f);
    return false;
  }
  Mf_toberead = read32bit();
  Mf_bytesread = 0;
  format = read16bit();
  ntrks = read16bit();
  division = read16bit();
  if (debug) printf("format=%d ntrks=%d division=%d\n",format,ntrks,division);

  while (Mf_toberead > 0) // flush any extra stuff
    egetc();
  track=1;
  bool init_ok=true;
  while ((init_ok=readtrack())==true) {
    track++;
    if (track>ntrks) break;
  }
  fclose(midi_f);
  if (gm_map) fclose(gm_map); // then: read_mapf
  if (!init_ok) return false;
  if (or_alert)
    alert("warning: out-of-range percussion instruments");
  return true;
}
